mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Snapshot testing progress
This commit is contained in:
@@ -4,7 +4,7 @@ from textual.widgets import Static
|
||||
|
||||
class GridLayoutExample(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Static("One", classes="box")
|
||||
yield Static("Oneeee", classes="box")
|
||||
yield Static("Two", classes="box")
|
||||
yield Static("Three", classes="box")
|
||||
yield Static("Four", classes="box")
|
||||
|
||||
104
poetry.lock
generated
104
poetry.lock
generated
@@ -50,14 +50,6 @@ category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "atomicwrites"
|
||||
version = "1.4.1"
|
||||
description = "Atomic file writes."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "22.1.0"
|
||||
@@ -67,10 +59,10 @@ optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[package.extras]
|
||||
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
|
||||
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
|
||||
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
|
||||
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"]
|
||||
dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"]
|
||||
docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
|
||||
tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"]
|
||||
tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
@@ -142,6 +134,14 @@ category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "colored"
|
||||
version = "1.4.3"
|
||||
description = "Simple library for color and formatting to terminal"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "commonmark"
|
||||
version = "0.9.1"
|
||||
@@ -204,7 +204,7 @@ python-versions = "*"
|
||||
python-dateutil = ">=2.8.1"
|
||||
|
||||
[package.extras]
|
||||
dev = ["twine", "markdown", "flake8", "wheel"]
|
||||
dev = ["flake8", "markdown", "twine", "wheel"]
|
||||
|
||||
[[package]]
|
||||
name = "griffe"
|
||||
@@ -252,9 +252,9 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
|
||||
zipp = ">=0.5"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"]
|
||||
docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"]
|
||||
perf = ["ipython"]
|
||||
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"]
|
||||
testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
@@ -488,8 +488,8 @@ optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"]
|
||||
test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"]
|
||||
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"]
|
||||
test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
@@ -562,18 +562,17 @@ optional = false
|
||||
python-versions = ">=3.6.8"
|
||||
|
||||
[package.extras]
|
||||
diagrams = ["railroad-diagrams", "jinja2"]
|
||||
diagrams = ["jinja2", "railroad-diagrams"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "6.2.5"
|
||||
version = "7.1.3"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
|
||||
attrs = ">=19.2.0"
|
||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
|
||||
@@ -581,10 +580,10 @@ iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=0.12,<2.0"
|
||||
py = ">=1.8.2"
|
||||
toml = "*"
|
||||
tomli = ">=1.0.0"
|
||||
|
||||
[package.extras]
|
||||
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
|
||||
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-aiohttp"
|
||||
@@ -615,7 +614,7 @@ pytest = ">=6.1.0"
|
||||
typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""}
|
||||
|
||||
[package.extras]
|
||||
testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
|
||||
testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-cov"
|
||||
@@ -631,7 +630,7 @@ pytest = ">=4.6"
|
||||
toml = "*"
|
||||
|
||||
[package.extras]
|
||||
testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"]
|
||||
testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
@@ -687,6 +686,18 @@ category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
|
||||
[[package]]
|
||||
name = "syrupy"
|
||||
version = "3.0.0"
|
||||
description = "PyTest Snapshot Test Utility"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7,<4"
|
||||
|
||||
[package.dependencies]
|
||||
colored = ">=1.3.92,<2.0.0"
|
||||
pytest = ">=5.1.0,<8.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "time-machine"
|
||||
version = "2.8.1"
|
||||
@@ -781,8 +792,8 @@ optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"]
|
||||
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"]
|
||||
docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"]
|
||||
testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
|
||||
|
||||
[extras]
|
||||
dev = ["aiohttp", "click", "msgpack"]
|
||||
@@ -790,7 +801,7 @@ dev = ["aiohttp", "click", "msgpack"]
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.7"
|
||||
content-hash = "61db56567f708cd9ca1c27f0e4a4b4aa3dd808fc8411f80967a90995d7fdd8c8"
|
||||
content-hash = "b28791133eec3bbb2debd610593c608c3ec62524be47e803fcbd31e55ddbffee"
|
||||
|
||||
[metadata.files]
|
||||
aiohttp = [
|
||||
@@ -879,7 +890,6 @@ asynctest = [
|
||||
{file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"},
|
||||
{file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"},
|
||||
]
|
||||
atomicwrites = []
|
||||
attrs = []
|
||||
black = [
|
||||
{file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"},
|
||||
@@ -914,7 +924,10 @@ cfgv = [
|
||||
{file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"},
|
||||
{file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"},
|
||||
]
|
||||
charset-normalizer = []
|
||||
charset-normalizer = [
|
||||
{file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"},
|
||||
{file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"},
|
||||
]
|
||||
click = [
|
||||
{file = "click-8.1.2-py3-none-any.whl", hash = "sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e"},
|
||||
{file = "click-8.1.2.tar.gz", hash = "sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72"},
|
||||
@@ -923,19 +936,28 @@ colorama = [
|
||||
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
|
||||
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
|
||||
]
|
||||
colored = [
|
||||
{file = "colored-1.4.3.tar.gz", hash = "sha256:b7b48b9f40e8a65bbb54813d5d79dd008dc8b8c5638d5bbfd30fc5a82e6def7a"},
|
||||
]
|
||||
commonmark = [
|
||||
{file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"},
|
||||
{file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"},
|
||||
]
|
||||
coverage = []
|
||||
distlib = []
|
||||
filelock = []
|
||||
filelock = [
|
||||
{file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"},
|
||||
{file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"},
|
||||
]
|
||||
frozenlist = []
|
||||
ghp-import = [
|
||||
{file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"},
|
||||
{file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"},
|
||||
]
|
||||
griffe = []
|
||||
griffe = [
|
||||
{file = "griffe-0.22.0-py3-none-any.whl", hash = "sha256:65c94cba634d6ad397c495b04ed5fd3f06d9b16c4f9f78bd63be9ea34d6b7113"},
|
||||
{file = "griffe-0.22.0.tar.gz", hash = "sha256:a3c25a2b7bf729ecee7cd455b4eff548f01c620b8f58a8097a800caad221f12e"},
|
||||
]
|
||||
identify = []
|
||||
idna = [
|
||||
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
|
||||
@@ -1008,7 +1030,10 @@ mkdocs-autorefs = [
|
||||
{file = "mkdocs-autorefs-0.4.1.tar.gz", hash = "sha256:70748a7bd025f9ecd6d6feeba8ba63f8e891a1af55f48e366d6d6e78493aba84"},
|
||||
{file = "mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"},
|
||||
]
|
||||
mkdocs-material = []
|
||||
mkdocs-material = [
|
||||
{file = "mkdocs-material-8.4.2.tar.gz", hash = "sha256:704c64c3fff126a3923c2961d95f26b19be621342a6a4e49ed039f0bb7a5c540"},
|
||||
{file = "mkdocs_material-8.4.2-py2.py3-none-any.whl", hash = "sha256:166287bb0e4197804906bf0389a852d5ced43182c30127ac8b48a4e497ecd7e5"},
|
||||
]
|
||||
mkdocs-material-extensions = [
|
||||
{file = "mkdocs-material-extensions-1.0.3.tar.gz", hash = "sha256:bfd24dfdef7b41c312ede42648f9eb83476ea168ec163b613f9abd12bbfddba2"},
|
||||
{file = "mkdocs_material_extensions-1.0.3-py3-none-any.whl", hash = "sha256:a82b70e533ce060b2a5d9eb2bc2e1be201cf61f901f93704b4acf6e3d5983a44"},
|
||||
@@ -1201,8 +1226,8 @@ pyparsing = [
|
||||
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
|
||||
]
|
||||
pytest = [
|
||||
{file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
|
||||
{file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
|
||||
{file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"},
|
||||
{file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"},
|
||||
]
|
||||
pytest-aiohttp = [
|
||||
{file = "pytest-aiohttp-1.0.4.tar.gz", hash = "sha256:39ff3a0d15484c01d1436cbedad575c6eafbf0f57cdf76fb94994c97b5b8c5a4"},
|
||||
@@ -1261,6 +1286,10 @@ six = [
|
||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||
]
|
||||
syrupy = [
|
||||
{file = "syrupy-3.0.0-py3-none-any.whl", hash = "sha256:e5e4d376766f46a52a3585365fb90aba0295611043a41a760302d2cd7aa74488"},
|
||||
{file = "syrupy-3.0.0.tar.gz", hash = "sha256:e1e2b6504c85c871fed89b82ce1d02442cf7b622b4b88f95c1332e71e012cd86"},
|
||||
]
|
||||
time-machine = []
|
||||
toml = [
|
||||
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
|
||||
@@ -1296,7 +1325,10 @@ typed-ast = [
|
||||
{file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"},
|
||||
{file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"},
|
||||
]
|
||||
typing-extensions = []
|
||||
typing-extensions = [
|
||||
{file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"},
|
||||
{file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"},
|
||||
]
|
||||
virtualenv = []
|
||||
watchdog = [
|
||||
{file = "watchdog-2.1.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a735a990a1095f75ca4f36ea2ef2752c99e6ee997c46b0de507ba40a09bf7330"},
|
||||
|
||||
@@ -37,7 +37,7 @@ nanoid = "^2.0.0"
|
||||
dev = ["aiohttp", "click", "msgpack"]
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pytest = "^6.2.3"
|
||||
pytest = "^7.1.3"
|
||||
black = "^22.3.0"
|
||||
mypy = "^0.950"
|
||||
pytest-cov = "^2.12.1"
|
||||
@@ -48,6 +48,7 @@ pre-commit = "^2.13.0"
|
||||
pytest-aiohttp = "^1.0.4"
|
||||
time-machine = "^2.6.0"
|
||||
Jinja2 = "<3.1.0"
|
||||
syrupy = "^3.0.0"
|
||||
|
||||
[tool.black]
|
||||
includes = "src"
|
||||
@@ -55,8 +56,10 @@ includes = "src"
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
testpaths = ["tests"]
|
||||
addopts = "--strict-markers"
|
||||
markers = [
|
||||
"integration_test: marks tests as slow integration tests (deselect with '-m \"not integration_test\"')",
|
||||
"snapshot: marks test as a snapshot test",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
|
||||
@@ -3,15 +3,16 @@ from __future__ import annotations
|
||||
import runpy
|
||||
import os
|
||||
import shlex
|
||||
from typing import cast, TYPE_CHECKING
|
||||
from typing import cast, TYPE_CHECKING, Iterable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from textual.app import App
|
||||
|
||||
|
||||
# This module defines our "Custom Fences", powered by SuperFences
|
||||
# @link https://facelessuser.github.io/pymdown-extensions/extensions/superfences/#custom-fences
|
||||
def format_svg(source, language, css_class, options, md, attrs, **kwargs) -> str:
|
||||
"""A superfences formatter to insert a SVG screenshot."""
|
||||
"""A superfences formatter to insert an SVG screenshot."""
|
||||
|
||||
try:
|
||||
cmd: list[str] = shlex.split(attrs["path"])
|
||||
@@ -21,25 +22,13 @@ def format_svg(source, language, css_class, options, md, attrs, **kwargs) -> str
|
||||
press = [*_press.split(",")] if _press else ["_"]
|
||||
title = attrs.get("title")
|
||||
|
||||
os.environ["COLUMNS"] = attrs.get("columns", "80")
|
||||
os.environ["LINES"] = attrs.get("lines", "24")
|
||||
|
||||
print(f"screenshotting {path!r}")
|
||||
|
||||
cwd = os.getcwd()
|
||||
try:
|
||||
app_vars = runpy.run_path(path)
|
||||
if "sys" in app_vars:
|
||||
app_vars["sys"].argv = cmd
|
||||
app: App = cast("App", app_vars["app"])
|
||||
app.run(
|
||||
quit_after=5,
|
||||
press=press or ["ctrl+c"],
|
||||
headless=True,
|
||||
screenshot=True,
|
||||
screenshot_title=title,
|
||||
)
|
||||
svg = app._screenshot
|
||||
rows = int(attrs.get("lines", 24))
|
||||
columns = int(attrs.get("columns", 80))
|
||||
svg = take_svg_screenshot(path, press, title, terminal_size=(rows, columns))
|
||||
finally:
|
||||
os.chdir(cwd)
|
||||
|
||||
@@ -52,8 +41,40 @@ def format_svg(source, language, css_class, options, md, attrs, **kwargs) -> str
|
||||
traceback.print_exception(error)
|
||||
|
||||
|
||||
def take_svg_screenshot(
|
||||
app_path: str,
|
||||
press: Iterable[str] = ("_",),
|
||||
title: str | None = None,
|
||||
terminal_size: tuple[int, int] = (24, 80),
|
||||
) -> str:
|
||||
rows, columns = terminal_size
|
||||
|
||||
os.environ["COLUMNS"] = str(columns)
|
||||
os.environ["LINES"] = str(rows)
|
||||
|
||||
app_vars = runpy.run_path(app_path)
|
||||
if "sys" in app_vars:
|
||||
cmd: list[str] = shlex.split(app_path)
|
||||
app_vars["sys"].argv = cmd
|
||||
|
||||
app: App = cast("App", app_vars["app"])
|
||||
|
||||
if title is None:
|
||||
title = app.title
|
||||
|
||||
app.run(
|
||||
quit_after=5,
|
||||
press=press or ["ctrl+c"],
|
||||
headless=True,
|
||||
screenshot=True,
|
||||
screenshot_title=title,
|
||||
)
|
||||
svg = app._screenshot
|
||||
return svg
|
||||
|
||||
|
||||
def rich(source, language, css_class, options, md, attrs, **kwargs) -> str:
|
||||
"""A superfences formatter to insert a SVG screenshot."""
|
||||
"""A superfences formatter to insert an SVG screenshot."""
|
||||
|
||||
from rich.console import Console
|
||||
import io
|
||||
|
||||
0
tests/snapshot_tests/__init__.py
Normal file
0
tests/snapshot_tests/__init__.py
Normal file
480
tests/snapshot_tests/__snapshots__/test_snapshots.ambr
Normal file
480
tests/snapshot_tests/__snapshots__/test_snapshots.ambr
Normal file
File diff suppressed because one or more lines are too long
178
tests/snapshot_tests/conftest.py
Normal file
178
tests/snapshot_tests/conftest.py
Normal file
@@ -0,0 +1,178 @@
|
||||
import difflib
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
from operator import attrgetter
|
||||
from os import PathLike
|
||||
from pathlib import Path
|
||||
from typing import Union, List, Optional, Any, Callable
|
||||
|
||||
import pytest
|
||||
from _pytest.fixtures import FixtureDef, SubRequest, FixtureRequest
|
||||
from jinja2 import Template
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
|
||||
from textual._doc import take_svg_screenshot
|
||||
|
||||
snapshot_svg_key = pytest.StashKey[str]()
|
||||
actual_svg_key = pytest.StashKey[str]()
|
||||
snapshot_pass = pytest.StashKey[bool]()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def snap_compare(snapshot, request: FixtureRequest) -> Callable[[str], bool]:
|
||||
def compare(app_path: str, snapshot) -> bool:
|
||||
node = request.node
|
||||
actual_screenshot = take_svg_screenshot(app_path)
|
||||
result = snapshot == actual_screenshot
|
||||
|
||||
if result is False:
|
||||
# The split and join below is a mad hack, sorry...
|
||||
node.stash[snapshot_svg_key] = "\n".join(str(snapshot).splitlines()[1:-1])
|
||||
node.stash[actual_svg_key] = actual_screenshot
|
||||
else:
|
||||
node.stash[snapshot_pass] = True
|
||||
|
||||
return result
|
||||
|
||||
return partial(compare, snapshot=snapshot)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SvgSnapshotDiff:
|
||||
snapshot: Optional[str]
|
||||
actual: Optional[str]
|
||||
test_name: str
|
||||
file_similarity: float
|
||||
path: PathLike
|
||||
line_number: int
|
||||
|
||||
|
||||
#
|
||||
# def pytest_runtestloop(session: "Session") -> Optional[object]:
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_pyfunc_call(pyfuncitem: "pytest.Function") -> Optional[object]:
|
||||
"""Call underlying test function.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult`.
|
||||
"""
|
||||
# Before
|
||||
yield
|
||||
# After
|
||||
|
||||
|
||||
def pytest_runtest_teardown(item: pytest.Item, nextitem: Optional[pytest.Item]) -> None:
|
||||
"""Called to perform the teardown phase for a test item.
|
||||
|
||||
The default implementation runs the finalizers and calls ``teardown()``
|
||||
on ``item`` and all of its parents (which need to be torn down). This
|
||||
includes running the teardown phase of fixtures required by the item (if
|
||||
they go out of scope).
|
||||
|
||||
:param nextitem:
|
||||
The scheduled-to-be-next test item (None if no further test item is
|
||||
scheduled). This argument is used to perform exact teardowns, i.e.
|
||||
calling just enough finalizers so that nextitem only needs to call
|
||||
setup functions.
|
||||
"""
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_fixture_setup(
|
||||
fixturedef: FixtureDef[Any], request: SubRequest
|
||||
) -> Optional[object]:
|
||||
"""Perform fixture setup execution.
|
||||
|
||||
:returns: The return value of the call to the fixture function.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult`.
|
||||
|
||||
.. note::
|
||||
If the fixture function returns None, other implementations of
|
||||
this hook function will continue to be called, according to the
|
||||
behavior of the :ref:`firstresult` option.
|
||||
"""
|
||||
value = yield
|
||||
value = value.get_result()
|
||||
value = repr(value)
|
||||
|
||||
|
||||
def pytest_sessionfinish(
|
||||
session: "pytest.Session",
|
||||
exitstatus: Union[int, "pytest.ExitCode"],
|
||||
) -> None:
|
||||
"""Called after whole test run finished, right before returning the exit status to the system.
|
||||
|
||||
:param pytest.Session session: The pytest session object.
|
||||
:param int exitstatus: The status which pytest will return to the system.
|
||||
"""
|
||||
diffs: List[SvgSnapshotDiff] = []
|
||||
num_snapshots_passing = 0
|
||||
for item in session.items:
|
||||
num_snapshots_passing += int(item.stash.get(snapshot_pass, False))
|
||||
snapshot_svg = item.stash.get(snapshot_svg_key, None)
|
||||
actual_svg = item.stash.get(actual_svg_key, None)
|
||||
if snapshot_svg and actual_svg:
|
||||
path, line_index, name = item.reportinfo()
|
||||
diffs.append(
|
||||
SvgSnapshotDiff(
|
||||
snapshot=str(snapshot_svg),
|
||||
actual=str(actual_svg),
|
||||
file_similarity=100 * difflib.SequenceMatcher(a=str(snapshot_svg), b=str(actual_svg)).ratio(),
|
||||
test_name=name,
|
||||
path=path,
|
||||
line_number=line_index + 1,
|
||||
)
|
||||
)
|
||||
|
||||
diff_sort_key = attrgetter("file_similarity")
|
||||
diffs = sorted(diffs, key=diff_sort_key)
|
||||
|
||||
conftest_path = Path(__file__)
|
||||
snapshot_template_path = conftest_path.parent / "snapshot_report_template.jinja2"
|
||||
snapshot_report_path = conftest_path.parent / "snapshot_report.html"
|
||||
|
||||
template = Template(snapshot_template_path.read_text())
|
||||
|
||||
num_fails = len(diffs)
|
||||
num_snapshot_tests = len(diffs) + num_snapshots_passing
|
||||
|
||||
rendered_report = template.render(
|
||||
diffs=diffs,
|
||||
passes=num_snapshots_passing,
|
||||
fails=num_fails,
|
||||
pass_percentage=100*(num_snapshots_passing/num_snapshot_tests),
|
||||
fail_percentage=100*(num_fails/num_snapshot_tests),
|
||||
num_snapshot_tests=num_snapshot_tests,
|
||||
now=datetime.utcnow()
|
||||
)
|
||||
with open(snapshot_report_path, "wt") as snapshot_file:
|
||||
snapshot_file.write(rendered_report)
|
||||
|
||||
session.config._textual_snapshots = diffs
|
||||
session.config._textual_snapshot_html_report = snapshot_report_path
|
||||
|
||||
|
||||
def pytest_terminal_summary(
|
||||
terminalreporter: "pytest.TerminalReporter",
|
||||
exitstatus: pytest.ExitCode,
|
||||
config: pytest.Config,
|
||||
) -> None:
|
||||
"""Add a section to terminal summary reporting.
|
||||
|
||||
:param _pytest.terminal.TerminalReporter terminalreporter: The internal terminal reporter object.
|
||||
:param int exitstatus: The exit status that will be reported back to the OS.
|
||||
:param pytest.Config config: The pytest config object.
|
||||
|
||||
.. versionadded:: 4.2
|
||||
The ``config`` parameter.
|
||||
"""
|
||||
diffs = config._textual_snapshots
|
||||
snapshot_report_location = config._textual_snapshot_html_report
|
||||
console = Console()
|
||||
summary_panel = Panel(
|
||||
f"[b]Report available for {len(diffs)} snapshot test failures.[/]\n\nView the report at:\n\n[blue]{snapshot_report_location}[/]",
|
||||
title="[b red]Textual Snapshot Test Summary", padding=1)
|
||||
console.print(summary_panel)
|
||||
412
tests/snapshot_tests/snapshot_report.html
Normal file
412
tests/snapshot_tests/snapshot_report.html
Normal file
File diff suppressed because one or more lines are too long
98
tests/snapshot_tests/snapshot_report_template.jinja2
Normal file
98
tests/snapshot_tests/snapshot_report_template.jinja2
Normal file
@@ -0,0 +1,98 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Textual Snapshot Test Report</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT" crossorigin="anonymous">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-4" style="background-color:#F4F8F7;">
|
||||
<div class="col-8 p-4">
|
||||
<h4>
|
||||
<strong>Textual</strong> Snapshot Tests
|
||||
</h4>
|
||||
<span class="text-muted">Showing diffs for {{ fails }} mismatched snapshot(s)</span>
|
||||
</div>
|
||||
<div class="col p-4">
|
||||
<div class="w-100 d-flex justify-content-end mb-1 mt-2">
|
||||
<span class="text-danger">
|
||||
<strong>{{ diffs | length }}</strong> snapshots changed
|
||||
</span>
|
||||
<span class="text-muted mx-2">
|
||||
·
|
||||
</span>
|
||||
<span class="text-success">
|
||||
<strong>{{ passes }}</strong> snapshots matched
|
||||
</span>
|
||||
</div>
|
||||
<div class="progress">
|
||||
<div class="progress-bar bg-danger" role="progressbar" aria-label="Segment one"
|
||||
style="width: {{ fail_percentage }}%"
|
||||
aria-valuenow="{{ fails }}" aria-valuemin="0" aria-valuemax="{{ num_snapshot_tests }}"></div>
|
||||
<div class="progress-bar bg-success" role="progressbar" aria-label="Segment two"
|
||||
style="width: {{ pass_percentage }}%"
|
||||
aria-valuenow="{{ num_snapshot_tests }}" aria-valuemin="0"
|
||||
aria-valuemax="{{ num_snapshot_tests }}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% for diff in diffs %}
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between">
|
||||
<div>
|
||||
<strong class="font-monospace">
|
||||
{{ diff.test_name }}
|
||||
</strong>
|
||||
<span class="text-muted">({{ "%.2f"|format(diff.file_similarity) }}% similar)</span>
|
||||
</div>
|
||||
<span class="text-muted">{{ diff.path }}:{{ diff.line_number }}</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{{ diff.actual }}
|
||||
<div class="w-100 d-flex justify-content-center mt-1">
|
||||
<span class="small">Output from test</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
{{ diff.snapshot }}
|
||||
<div class="w-100 d-flex justify-content-center mt-1">
|
||||
<span class="small">Historical snapshot</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="row" style="background-color:#F4F8F7;">
|
||||
<div class="col">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body">
|
||||
<p class="card-text">If you're happy with the change, run pytest with the <span class="font-monospace text-primary">--snapshot-update</span> flag to update the snapshot.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="w-100 d-flex p-4 justify-content-center">
|
||||
<p class="text-muted">Report generated at UTC {{ now }}.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
14
tests/snapshot_tests/test_snapshots.py
Normal file
14
tests/snapshot_tests/test_snapshots.py
Normal file
@@ -0,0 +1,14 @@
|
||||
def test_grid_layout_basic(snap_compare):
|
||||
assert snap_compare("docs/examples/guide/layout/grid_layout1.py")
|
||||
|
||||
|
||||
def test_grid_layout_basic_overflow(snap_compare):
|
||||
assert snap_compare("docs/examples/guide/layout/grid_layout2.py")
|
||||
|
||||
|
||||
def test_combining_layouts(snap_compare):
|
||||
assert snap_compare("docs/examples/guide/layout/combining_layouts.py")
|
||||
|
||||
|
||||
def test_layers(snap_compare):
|
||||
assert snap_compare("docs/examples/guide/layout/layers.py")
|
||||
Reference in New Issue
Block a user