From e981e3b99cbe3593999f9cfa917b48754e275e46 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 15 Sep 2022 13:10:27 +0100 Subject: [PATCH 01/46] Add docs for CSS variables --- docs/guide/CSS.md | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/docs/guide/CSS.md b/docs/guide/CSS.md index 4025d0a92..8b7edae12 100644 --- a/docs/guide/CSS.md +++ b/docs/guide/CSS.md @@ -238,7 +238,7 @@ You can match an ID with a selector starting with a hash (`#`). Here is how you } ``` -A Widget's `id` attribute can not be changed after the Widget has been constructed. +A Widget's `id` attribute can not be changed after the Widget has been constructed. ### Class-name selector @@ -395,4 +395,45 @@ Button:hover { ## CSS Variables -TODO: Variables +You can define variables to reduce repetition and encourage consistency in your CSS. +Variables in Textual CSS are prefixed with `$`. +Here's an example of how you might define a variable called `$border`: + +```scss +$border: wide green; +``` + +With our variable assigned, we can now write `$border` and it will be substituted with `wide green`. +For example, consider the following snippet: + +```scss +#foo { + border: $border; +} +``` + +This will be translated into: + +```scss +#foo { + border: wide green; +} +``` + +Variables allow us to define reusable styling in a single place. +If we decide we want to change some aspect of our design in the future, we only have to update a single variable. + +!!! note + + Variables can only be used in the _values_ of a CSS declaration. You cannot, for example, refer to a variable insid ea + +Variables can also refer to other variables. +For example, say we define a variable `$success: lime;`. +Our `$border` variable could then be updated to `$border: wide $success;`, which will +be translated to `$border: wide lime;`. + +Textual CSS ships with a number of builtin variables. +These can be used in CSS without any additional imports or declarations. +For more information on these builtin variables, see [this page](#). + +[//]: # (TODO: Fill in the link above when builtin style variables are documented) From d619dae510a9a790c6a4ba4b963928798291eb53 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Fri, 16 Sep 2022 16:28:48 +0100 Subject: [PATCH 02/46] Snapshot testing progress --- docs/examples/guide/layout/grid_layout2.py | 2 +- poetry.lock | 104 ++-- pyproject.toml | 5 +- src/textual/_doc.py | 57 ++- tests/snapshot_tests/__init__.py | 0 .../__snapshots__/test_snapshots.ambr | 480 ++++++++++++++++++ tests/snapshot_tests/conftest.py | 178 +++++++ tests/snapshot_tests/snapshot_report.html | 412 +++++++++++++++ .../snapshot_report_template.jinja2 | 98 ++++ tests/snapshot_tests/test_snapshots.py | 14 + 10 files changed, 1294 insertions(+), 56 deletions(-) create mode 100644 tests/snapshot_tests/__init__.py create mode 100644 tests/snapshot_tests/__snapshots__/test_snapshots.ambr create mode 100644 tests/snapshot_tests/conftest.py create mode 100644 tests/snapshot_tests/snapshot_report.html create mode 100644 tests/snapshot_tests/snapshot_report_template.jinja2 create mode 100644 tests/snapshot_tests/test_snapshots.py diff --git a/docs/examples/guide/layout/grid_layout2.py b/docs/examples/guide/layout/grid_layout2.py index 825d6e1c1..ed8f697b7 100644 --- a/docs/examples/guide/layout/grid_layout2.py +++ b/docs/examples/guide/layout/grid_layout2.py @@ -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") diff --git a/poetry.lock b/poetry.lock index c6c1d2d27..ea583c97a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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"}, diff --git a/pyproject.toml b/pyproject.toml index 71eb976e5..e03c8828c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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] diff --git a/src/textual/_doc.py b/src/textual/_doc.py index b70ce67fc..8c2901dd4 100644 --- a/src/textual/_doc.py +++ b/src/textual/_doc.py @@ -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 diff --git a/tests/snapshot_tests/__init__.py b/tests/snapshot_tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr new file mode 100644 index 000000000..74538c10d --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -0,0 +1,480 @@ +# name: test_combining_layouts + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Textual + + + + + + + + + + + CombiningLayoutsExample14:29:12 + + ┌──────────────────────────────────────┐┌──────────────────────────────────────┐ + HorizontallyPositionedChildrenHere + Vertical layout, child 0 + + + + Vertical layout, child 1 + ▇▇ + + └──────────────────────────────────────┘ + Vertical layout, child 2┌──────────────────────────────────────┐ + Thispanelis + + + Vertical layout, child 3 + + usinggrid layout! + + Vertical layout, child 4 + + └──────────────────────────────────────┘└──────────────────────────────────────┘ + + + + + ''' +# --- +# name: test_grid_layout_basic + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Textual + + + + + + + + + + ┌────────────────────────┐┌─────────────────────────┐┌─────────────────────────┐ + OneTwoThree + + + + + + + + + + └────────────────────────┘└─────────────────────────┘└─────────────────────────┘ + ┌────────────────────────┐┌─────────────────────────┐┌─────────────────────────┐ + FourFiveSix + + + + + + + + + + └────────────────────────┘└─────────────────────────┘└─────────────────────────┘ + + + + + ''' +# --- +# name: test_grid_layout_basic_overflow + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Textual + + + + + + + + + + ┌────────────────────────┐┌─────────────────────────┐┌─────────────────────────┐ + OneeeeTwoThree + + + + + + └────────────────────────┘└─────────────────────────┘└─────────────────────────┘ + ┌────────────────────────┐┌─────────────────────────┐┌─────────────────────────┐ + FourFiveSix + + + + + + └────────────────────────┘└─────────────────────────┘└─────────────────────────┘ + ┌────────────────────────┐ + Seven + + + + + + └────────────────────────┘ + + + + + ''' +# --- +# name: test_x + ''' + 123 + 123123123 + ''' +# --- diff --git a/tests/snapshot_tests/conftest.py b/tests/snapshot_tests/conftest.py new file mode 100644 index 000000000..f24ab3512 --- /dev/null +++ b/tests/snapshot_tests/conftest.py @@ -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) diff --git a/tests/snapshot_tests/snapshot_report.html b/tests/snapshot_tests/snapshot_report.html new file mode 100644 index 000000000..f853befd7 --- /dev/null +++ b/tests/snapshot_tests/snapshot_report.html @@ -0,0 +1,412 @@ + + + + + + Textual Snapshot Test Report + + + +
+
+
+

+ Textual Snapshot Tests +

+ Showing diffs for 1 mismatched snapshot(s) +
+
+
+ + 1 snapshots changed + + + · + + + 2 snapshots matched + +
+
+
+
+
+
+
+ + +
+
+
+
+
+ + test_combining_layouts + + (88.89% similar) +
+ /Users/darrenburns/code/textual/tests/snapshot_tests/test_snapshots.py:9 +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Textual + + + + + + + + + + +CombiningLayoutsExample15:50:46 + +┌──────────────────────────────────────┐┌──────────────────────────────────────┐ +HorizontallyPositionedChildrenHere +Vertical layout, child 0 + + + +Vertical layout, child 1 +▇▇ + +└──────────────────────────────────────┘ +Vertical layout, child 2┌──────────────────────────────────────┐ +Thispanelis + + +Vertical layout, child 3 + +usinggrid layout! + +Vertical layout, child 4 + +└──────────────────────────────────────┘└──────────────────────────────────────┘ + + + + +
+ Output from test +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Textual + + + + + + + + + + +CombiningLayoutsExample14:29:12 + +┌──────────────────────────────────────┐┌──────────────────────────────────────┐ +HorizontallyPositionedChildrenHere +Vertical layout, child 0 + + + +Vertical layout, child 1 +▇▇ + +└──────────────────────────────────────┘ +Vertical layout, child 2┌──────────────────────────────────────┐ +Thispanelis + + +Vertical layout, child 3 + +usinggrid layout! + +Vertical layout, child 4 + +└──────────────────────────────────────┘└──────────────────────────────────────┘ + + + + +
+ Historical snapshot +
+
+
+
+
+
+
+ + +
+
+
+
+

If you're happy with the change, run pytest with the --snapshot-update flag to update the snapshot.

+
+
+
+
+ +
+
+
+

Report generated at UTC 2022-09-16 14:50:47.993944.

+
+
+
+ +
+ + + diff --git a/tests/snapshot_tests/snapshot_report_template.jinja2 b/tests/snapshot_tests/snapshot_report_template.jinja2 new file mode 100644 index 000000000..a726b9b57 --- /dev/null +++ b/tests/snapshot_tests/snapshot_report_template.jinja2 @@ -0,0 +1,98 @@ + + + + + + Textual Snapshot Test Report + + + +
+
+
+

+ Textual Snapshot Tests +

+ Showing diffs for {{ fails }} mismatched snapshot(s) +
+
+
+ + {{ diffs | length }} snapshots changed + + + · + + + {{ passes }} snapshots matched + +
+
+
+
+
+
+
+ + {% for diff in diffs %} +
+
+
+
+
+ + {{ diff.test_name }} + + ({{ "%.2f"|format(diff.file_similarity) }}% similar) +
+ {{ diff.path }}:{{ diff.line_number }} +
+
+
+
+ {{ diff.actual }} +
+ Output from test +
+
+
+ {{ diff.snapshot }} +
+ Historical snapshot +
+
+
+
+
+
+
+ {% endfor %} + +
+
+
+
+

If you're happy with the change, run pytest with the --snapshot-update flag to update the snapshot.

+
+
+
+
+ +
+
+
+

Report generated at UTC {{ now }}.

+
+
+
+ +
+ + + diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py new file mode 100644 index 000000000..d3de89f48 --- /dev/null +++ b/tests/snapshot_tests/test_snapshots.py @@ -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") From 13b27e1fa33cdce2adfe61c16712d086b8cc21bf Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 20 Sep 2022 13:48:35 +0100 Subject: [PATCH 03/46] Updates --- .../__snapshots__/test_snapshots.ambr | 747 ++++++++++++++++-- tests/snapshot_tests/conftest.py | 46 +- tests/snapshot_tests/snapshot_report.html | 364 +-------- .../snapshot_report_template.jinja2 | 2 +- tests/snapshot_tests/test_snapshots.py | 20 +- 5 files changed, 715 insertions(+), 464 deletions(-) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 74538c10d..e333fcd1f 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -1,4 +1,4 @@ -# name: test_combining_layouts +# name: test_center_layout ''' @@ -21,139 +21,289 @@ font-weight: 700; } - .terminal-1942842461-matrix { + .terminal-3376477890-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1942842461-title { + .terminal-3376477890-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1942842461-r1 { fill: #c5c8c6 } - .terminal-1942842461-r2 { fill: #f6f5f5 } - .terminal-1942842461-r3 { fill: #e0dedb } - .terminal-1942842461-r4 { fill: #1e90ff } - .terminal-1942842461-r5 { fill: #c71585 } - .terminal-1942842461-r6 { fill: #f3f3f3 } - .terminal-1942842461-r7 { fill: #ffffff } - .terminal-1942842461-r8 { fill: #172127 } - .terminal-1942842461-r9 { fill: #adff2f } + .terminal-3376477890-r1 { fill: #f3f3f3 } + .terminal-3376477890-r2 { fill: #c5c8c6 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - Textual + Textual - - - - - CombiningLayoutsExample14:29:12 - - ┌──────────────────────────────────────┐┌──────────────────────────────────────┐ - HorizontallyPositionedChildrenHere - Vertical layout, child 0 - - - - Vertical layout, child 1 - ▇▇ - - └──────────────────────────────────────┘ - Vertical layout, child 2┌──────────────────────────────────────┐ - Thispanelis - - - Vertical layout, child 3 - - usinggrid layout! - - Vertical layout, child 4 - - └──────────────────────────────────────┘└──────────────────────────────────────┘ + + + + + + One + + + + Two + + + Three + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_dock_layout_sidebar + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Textual + + + + + + + + + + Sidebar1Docking a widget removes it from the layout and  + fixes its position, aligned to either the top,  + right, bottom, or left edges of a container. + + Docked widgets will not scroll out of view,  + making them ideal for sticky headers, footers,  + and sidebars. + ▇▇ + Docking a widget removes it from the layout and  + fixes its position, aligned to either the top,  + right, bottom, or left edges of a container. + + Docked widgets will not scroll out of view,  + making them ideal for sticky headers, footers,  + and sidebars. + + Docking a widget removes it from the layout and  + fixes its position, aligned to either the top,  + right, bottom, or left edges of a container. + + Docked widgets will not scroll out of view,  + making them ideal for sticky headers, footers,  + and sidebars. + @@ -472,9 +622,472 @@ ''' # --- -# name: test_x +# name: test_horizontal_layout ''' - 123 - 123123123 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Textual + + + + + + + + + + ┌────────────────────────┐┌─────────────────────────┐┌─────────────────────────┐ + OneTwoThree + + + + + + + + + + + + + + + + + + + + + + └────────────────────────┘└─────────────────────────┘└─────────────────────────┘ + + + + + ''' +# --- +# name: test_layers + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Textual + + + + + + + + + + + + + + + + + + + + + box1 (layer = above) + + + + + + box2 (layer = below) + + + + + + + + + + + ''' +# --- +# name: test_vertical_layout + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Textual + + + + + + + + + + ┌──────────────────────────────────────────────────────────────────────────────┐ + One + + + + + + └──────────────────────────────────────────────────────────────────────────────┘ + ┌──────────────────────────────────────────────────────────────────────────────┐ + Two + + + + + + └──────────────────────────────────────────────────────────────────────────────┘ + ┌──────────────────────────────────────────────────────────────────────────────┐ + Three + + + + + + └──────────────────────────────────────────────────────────────────────────────┘ + + + + ''' # --- diff --git a/tests/snapshot_tests/conftest.py b/tests/snapshot_tests/conftest.py index f24ab3512..69cc6d1b3 100644 --- a/tests/snapshot_tests/conftest.py +++ b/tests/snapshot_tests/conftest.py @@ -5,10 +5,10 @@ from functools import partial from operator import attrgetter from os import PathLike from pathlib import Path -from typing import Union, List, Optional, Any, Callable +from typing import Union, List, Optional, Callable import pytest -from _pytest.fixtures import FixtureDef, SubRequest, FixtureRequest +from _pytest.fixtures import FixtureRequest from jinja2 import Template from rich.console import Console from rich.panel import Panel @@ -49,20 +49,6 @@ class SvgSnapshotDiff: 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. @@ -79,26 +65,6 @@ def pytest_runtest_teardown(item: pytest.Item, nextitem: Optional[pytest.Item]) """ -@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"], @@ -120,7 +86,9 @@ def pytest_sessionfinish( SvgSnapshotDiff( snapshot=str(snapshot_svg), actual=str(actual_svg), - file_similarity=100 * difflib.SequenceMatcher(a=str(snapshot_svg), b=str(actual_svg)).ratio(), + file_similarity=100 * difflib.SequenceMatcher(a=str(snapshot_svg), + b=str( + actual_svg)).ratio(), test_name=name, path=path, line_number=line_index + 1, @@ -143,8 +111,8 @@ def pytest_sessionfinish( 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), + pass_percentage=100 * (num_snapshots_passing / (num_snapshot_tests + 1)), + fail_percentage=100 * (num_fails / (num_snapshot_tests + 1)), num_snapshot_tests=num_snapshot_tests, now=datetime.utcnow() ) diff --git a/tests/snapshot_tests/snapshot_report.html b/tests/snapshot_tests/snapshot_report.html index f853befd7..294be0c2b 100644 --- a/tests/snapshot_tests/snapshot_report.html +++ b/tests/snapshot_tests/snapshot_report.html @@ -14,379 +14,33 @@

Textual Snapshot Tests

- Showing diffs for 1 mismatched snapshot(s) + Showing diffs for 0 mismatched snapshot(s)
- 1 snapshots changed + 0 snapshots changed · - 2 snapshots matched + 7 snapshots matched
+ style="width: 0.0%" + aria-valuenow="0" aria-valuemin="0" aria-valuemax="7">
+ style="width: 87.5%" + aria-valuenow="7" aria-valuemin="0" + aria-valuemax="7">
-
-
-
-
-
- - test_combining_layouts - - (88.89% similar) -
- /Users/darrenburns/code/textual/tests/snapshot_tests/test_snapshots.py:9 -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Textual - - - - - - - - - - -CombiningLayoutsExample15:50:46 - -┌──────────────────────────────────────┐┌──────────────────────────────────────┐ -HorizontallyPositionedChildrenHere -Vertical layout, child 0 - - - -Vertical layout, child 1 -▇▇ - -└──────────────────────────────────────┘ -Vertical layout, child 2┌──────────────────────────────────────┐ -Thispanelis - - -Vertical layout, child 3 - -usinggrid layout! - -Vertical layout, child 4 - -└──────────────────────────────────────┘└──────────────────────────────────────┘ - - - - -
- Output from test -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Textual - - - - - - - - - - -CombiningLayoutsExample14:29:12 - -┌──────────────────────────────────────┐┌──────────────────────────────────────┐ -HorizontallyPositionedChildrenHere -Vertical layout, child 0 - - - -Vertical layout, child 1 -▇▇ - -└──────────────────────────────────────┘ -Vertical layout, child 2┌──────────────────────────────────────┐ -Thispanelis - - -Vertical layout, child 3 - -usinggrid layout! - -Vertical layout, child 4 - -└──────────────────────────────────────┘└──────────────────────────────────────┘ - - - - -
- Historical snapshot -
-
-
-
-
-
-
-
@@ -401,7 +55,7 @@
-

Report generated at UTC 2022-09-16 14:50:47.993944.

+

Report generated at UTC 2022-09-20 12:27:59.430929.

diff --git a/tests/snapshot_tests/snapshot_report_template.jinja2 b/tests/snapshot_tests/snapshot_report_template.jinja2 index a726b9b57..9427a9606 100644 --- a/tests/snapshot_tests/snapshot_report_template.jinja2 +++ b/tests/snapshot_tests/snapshot_report_template.jinja2 @@ -49,7 +49,7 @@ {{ diff.test_name }} - ({{ "%.2f"|format(diff.file_similarity) }}% similar) + ({{ "%.2f"|format(diff.file_similarity) }}% source similarity)
{{ diff.path }}:{{ diff.line_number }}
diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index d3de89f48..0c95f103c 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -6,9 +6,25 @@ 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_grid_layout_gutter(snap_compare): + assert snap_compare("docs/examples/guide/layout/grid_layout7_gutter.py") def test_layers(snap_compare): assert snap_compare("docs/examples/guide/layout/layers.py") + + +def test_center_layout(snap_compare): + assert snap_compare("docs/examples/guide/layout/center_layout.py") + + +def test_horizontal_layout(snap_compare): + assert snap_compare("docs/examples/guide/layout/horizontal_layout.py") + + +def test_vertical_layout(snap_compare): + assert snap_compare("docs/examples/guide/layout/vertical_layout.py") + + +def test_dock_layout_sidebar(snap_compare): + assert snap_compare("docs/examples/guide/layout/dock_layout2_sidebar.py") From c9cdb9746bfde083d9b97eaff3c2fd54db647d4f Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 20 Sep 2022 14:49:00 +0100 Subject: [PATCH 04/46] Fix off by one --- tests/snapshot_tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/snapshot_tests/conftest.py b/tests/snapshot_tests/conftest.py index b52bea564..e3c9ac95e 100644 --- a/tests/snapshot_tests/conftest.py +++ b/tests/snapshot_tests/conftest.py @@ -112,8 +112,8 @@ def pytest_sessionfinish( diffs=diffs, passes=num_snapshots_passing, fails=num_fails, - pass_percentage=100 * (num_snapshots_passing / (num_snapshot_tests + 1)), - fail_percentage=100 * (num_fails / (num_snapshot_tests + 1)), + pass_percentage=100 * (num_snapshots_passing / max(num_snapshot_tests, 1)), + fail_percentage=100 * (num_fails / max(num_snapshot_tests, 1)), num_snapshot_tests=num_snapshot_tests, now=datetime.utcnow() ) From a871e272b7b3ba37193215ee1e0c568ec713711f Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 20 Sep 2022 14:58:32 +0100 Subject: [PATCH 05/46] Update pre-commit-config to exclude snapshot tests --- .pre-commit-config.yaml | 1 + src/textual/_doc.py | 2 +- .../__snapshots__/test_snapshots.ambr | 273 ++++++++++++++---- tests/snapshot_tests/conftest.py | 2 +- tests/snapshot_tests/snapshot_report.html | 36 +-- 5 files changed, 235 insertions(+), 79 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b65350ed6..9e507b1e2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,3 +13,4 @@ repos: hooks: - id: black exclude: ^tests/ +exclude: ^tests/snapshot_tests diff --git a/src/textual/_doc.py b/src/textual/_doc.py index 0d1a4b9d8..0d8e9f9ec 100644 --- a/src/textual/_doc.py +++ b/src/textual/_doc.py @@ -46,7 +46,7 @@ def format_svg(source, language, css_class, options, md, attrs, **kwargs) -> str def take_svg_screenshot( app_path: str, - press: Iterable[str] = ("_",), + press: Iterable[str] = ("_", "_"), title: str | None = None, terminal_size: tuple[int, int] = (24, 80), ) -> str: diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index e333fcd1f..38cf4eb34 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -3,7 +3,7 @@ - + @@ -112,14 +112,14 @@ - + Textual - + @@ -150,7 +150,7 @@ - + ''' # --- # name: test_dock_layout_sidebar @@ -158,7 +158,7 @@ - + @@ -269,16 +269,16 @@ - + Textual - + - + Sidebar1Docking a widget removes it from the layout and  fixes its position, aligned to either the top,  @@ -307,7 +307,7 @@ - + ''' # --- # name: test_grid_layout_basic @@ -315,7 +315,7 @@ - + @@ -425,14 +425,14 @@ - + Textual - + @@ -463,7 +463,7 @@ - + ''' # --- # name: test_grid_layout_basic_overflow @@ -471,7 +471,7 @@ - + @@ -581,14 +581,14 @@ - + Textual - + @@ -619,15 +619,15 @@ - + ''' # --- -# name: test_horizontal_layout +# name: test_grid_layout_gutter ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Textual + + + + + + + + + + OneTwoThree + + + + + + + + + + + + FourFiveSix + + + + + + + + + + + + + + + + ''' +# --- +# name: test_horizontal_layout + ''' + + + - + @@ -737,14 +892,14 @@ - + Textual - + @@ -775,7 +930,7 @@ - + ''' # --- # name: test_layers @@ -783,7 +938,7 @@ - + @@ -894,14 +1049,14 @@ - + Textual - + @@ -932,7 +1087,7 @@ - + ''' # --- # name: test_vertical_layout @@ -940,7 +1095,7 @@ - + @@ -1050,14 +1205,14 @@ - + Textual - + @@ -1088,6 +1243,6 @@ - + ''' # --- diff --git a/tests/snapshot_tests/conftest.py b/tests/snapshot_tests/conftest.py index e3c9ac95e..02d94336e 100644 --- a/tests/snapshot_tests/conftest.py +++ b/tests/snapshot_tests/conftest.py @@ -138,7 +138,7 @@ def pytest_terminal_summary( .. versionadded:: 4.2 The ``config`` parameter. """ - diffs = config._textual_snapshots + diffs = getattr(config, "_textual_snapshots", None) if diffs: snapshot_report_location = config._textual_snapshot_html_report console = Console() diff --git a/tests/snapshot_tests/snapshot_report.html b/tests/snapshot_tests/snapshot_report.html index cb87d665e..3707f6dc1 100644 --- a/tests/snapshot_tests/snapshot_report.html +++ b/tests/snapshot_tests/snapshot_report.html @@ -30,7 +30,7 @@
- +
@@ -177,7 +177,7 @@ - + @@ -368,7 +368,7 @@
- +
@@ -503,7 +503,7 @@ - + @@ -692,7 +692,7 @@
- +
@@ -829,7 +829,7 @@ - + @@ -1020,7 +1020,7 @@
- +
@@ -1156,7 +1156,7 @@ - + @@ -1346,7 +1346,7 @@
- +
@@ -1482,7 +1482,7 @@ - + @@ -1672,7 +1672,7 @@
- +
@@ -1808,7 +1808,7 @@ - + @@ -1998,7 +1998,7 @@
- +
@@ -2134,7 +2134,7 @@ - + @@ -2324,7 +2324,7 @@
- +
@@ -2339,7 +2339,7 @@
-

Report generated at UTC 2022-09-20 13:45:47.790376.

+

Report generated at UTC 2022-09-20 13:52:10.785737.

@@ -2347,4 +2347,4 @@
- + \ No newline at end of file From c82a143f0e1ac5b9c555ea6c2c6dc76b82127841 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 20 Sep 2022 15:11:19 +0100 Subject: [PATCH 06/46] Snapshot tests output directory ignored --- .gitignore | 3 +++ tests/snapshot_tests/conftest.py | 4 ++-- tests/snapshot_tests/{ => output}/snapshot_report.html | 0 3 files changed, 5 insertions(+), 2 deletions(-) rename tests/snapshot_tests/{ => output}/snapshot_report.html (100%) diff --git a/.gitignore b/.gitignore index a20548735..4580aac9a 100644 --- a/.gitignore +++ b/.gitignore @@ -113,3 +113,6 @@ venv.bak/ # mypy .mypy_cache/ + +# Snapshot testing report output directory +tests/snapshot_tests/output diff --git a/tests/snapshot_tests/conftest.py b/tests/snapshot_tests/conftest.py index 02d94336e..f5b76de47 100644 --- a/tests/snapshot_tests/conftest.py +++ b/tests/snapshot_tests/conftest.py @@ -101,7 +101,7 @@ def pytest_sessionfinish( conftest_path = Path(__file__) snapshot_template_path = conftest_path.parent / "snapshot_report_template.jinja2" - snapshot_report_path = conftest_path.parent / "snapshot_report.html" + snapshot_report_path = conftest_path.parent / "output/snapshot_report.html" template = Template(snapshot_template_path.read_text()) @@ -139,9 +139,9 @@ def pytest_terminal_summary( The ``config`` parameter. """ diffs = getattr(config, "_textual_snapshots", None) + console = Console() if diffs: 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) diff --git a/tests/snapshot_tests/snapshot_report.html b/tests/snapshot_tests/output/snapshot_report.html similarity index 100% rename from tests/snapshot_tests/snapshot_report.html rename to tests/snapshot_tests/output/snapshot_report.html From 8f64e36caa0d14a9d348e9a6c8e144b42b3519c7 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 20 Sep 2022 15:13:37 +0100 Subject: [PATCH 07/46] Small tidy up --- tests/snapshot_tests/conftest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/snapshot_tests/conftest.py b/tests/snapshot_tests/conftest.py index f5b76de47..0ee6d379f 100644 --- a/tests/snapshot_tests/conftest.py +++ b/tests/snapshot_tests/conftest.py @@ -66,8 +66,8 @@ def pytest_runtest_teardown(item: pytest.Item, nextitem: Optional[pytest.Item]) def pytest_sessionfinish( - session: "pytest.Session", - exitstatus: Union[int, "pytest.ExitCode"], + session: pytest.Session, + exitstatus: Union[int, pytest.ExitCode], ) -> None: """Called after whole test run finished, right before returning the exit status to the system. @@ -125,7 +125,7 @@ def pytest_sessionfinish( def pytest_terminal_summary( - terminalreporter: "pytest.TerminalReporter", + terminalreporter: pytest.TerminalReporter, exitstatus: pytest.ExitCode, config: pytest.Config, ) -> None: From 4a4eeb503a04c229d4b34ed40e6c35b17bea81c6 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 20 Sep 2022 15:14:23 +0100 Subject: [PATCH 08/46] Most different snapshots first --- tests/snapshot_tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/snapshot_tests/conftest.py b/tests/snapshot_tests/conftest.py index 0ee6d379f..2d72cd585 100644 --- a/tests/snapshot_tests/conftest.py +++ b/tests/snapshot_tests/conftest.py @@ -97,7 +97,7 @@ def pytest_sessionfinish( if diffs: diff_sort_key = attrgetter("file_similarity") - diffs = sorted(diffs, key=diff_sort_key) + diffs = list(reversed(sorted(diffs, key=diff_sort_key))) conftest_path = Path(__file__) snapshot_template_path = conftest_path.parent / "snapshot_report_template.jinja2" From cdd2c01a752f337479bfab29b05dd2bbbe85c832 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 20 Sep 2022 15:19:56 +0100 Subject: [PATCH 09/46] Fix type imports from pytest --- tests/snapshot_tests/conftest.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/snapshot_tests/conftest.py b/tests/snapshot_tests/conftest.py index 2d72cd585..33933c887 100644 --- a/tests/snapshot_tests/conftest.py +++ b/tests/snapshot_tests/conftest.py @@ -8,7 +8,10 @@ from pathlib import Path from typing import Union, List, Optional, Callable import pytest +from _pytest.config import ExitCode from _pytest.fixtures import FixtureRequest +from _pytest.main import Session +from _pytest.terminal import TerminalReporter from jinja2 import Template from rich.console import Console from rich.panel import Panel @@ -66,8 +69,8 @@ def pytest_runtest_teardown(item: pytest.Item, nextitem: Optional[pytest.Item]) def pytest_sessionfinish( - session: pytest.Session, - exitstatus: Union[int, pytest.ExitCode], + session: Session, + exitstatus: Union[int, ExitCode], ) -> None: """Called after whole test run finished, right before returning the exit status to the system. @@ -125,8 +128,8 @@ def pytest_sessionfinish( def pytest_terminal_summary( - terminalreporter: pytest.TerminalReporter, - exitstatus: pytest.ExitCode, + terminalreporter: TerminalReporter, + exitstatus: ExitCode, config: pytest.Config, ) -> None: """Add a section to terminal summary reporting. From 863998173df1dbd94617ba2195282fa04f94c43d Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 20 Sep 2022 15:20:34 +0100 Subject: [PATCH 10/46] Remove unused pytest hookimpl --- tests/snapshot_tests/conftest.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tests/snapshot_tests/conftest.py b/tests/snapshot_tests/conftest.py index 33933c887..c9e7c86eb 100644 --- a/tests/snapshot_tests/conftest.py +++ b/tests/snapshot_tests/conftest.py @@ -52,22 +52,6 @@ class SvgSnapshotDiff: line_number: int -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. - """ - - def pytest_sessionfinish( session: Session, exitstatus: Union[int, ExitCode], From 0978eb51612ae7aa8a5439ac8b3d4939a5473166 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 20 Sep 2022 15:24:24 +0100 Subject: [PATCH 11/46] Remove snapshot_report from git --- .../output/snapshot_report.html | 2350 ----------------- 1 file changed, 2350 deletions(-) delete mode 100644 tests/snapshot_tests/output/snapshot_report.html diff --git a/tests/snapshot_tests/output/snapshot_report.html b/tests/snapshot_tests/output/snapshot_report.html deleted file mode 100644 index 3707f6dc1..000000000 --- a/tests/snapshot_tests/output/snapshot_report.html +++ /dev/null @@ -1,2350 +0,0 @@ - - - - - - Textual Snapshot Test Report - - - -
-
-
-

- Textual Snapshot Tests -

- Showing diffs for 7 mismatched snapshot(s) -
-
-
- - 7 snapshots changed - - - · - - - 0 snapshots matched - -
-
-
-
-
-
-
- - -
-
-
-
-
- - test_dock_layout_sidebar - - (76.21% source similarity) -
- /Users/darrenburns/Code/textual/tests/snapshot_tests/test_snapshots.py:29 -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Textual - - - - - - - - - - Sidebar1Docking a widget removes it from the layout and  -fixes its position, aligned to either the top,  -right, bottom, or left edges of a container. - -Docked widgets will not scroll out of view,  -making them ideal for sticky headers, footers,  -and sidebars. -▇▇ -Docking a widget removes it from the layout and  -fixes its position, aligned to either the top,  -right, bottom, or left edges of a container. - -Docked widgets will not scroll out of view,  -making them ideal for sticky headers, footers,  -and sidebars. - -Docking a widget removes it from the layout and  -fixes its position, aligned to either the top,  -right, bottom, or left edges of a container. - -Docked widgets will not scroll out of view,  -making them ideal for sticky headers, footers,  -and sidebars. - - - - - -
- Output from test -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Textual - - - - - - - - - Sidebar1Docking a widget removes it from the layout and  -fixes its position, aligned to either the top,  -right, bottom, or left edges of a container. - -Docked widgets will not scroll out of view,  -making them ideal for sticky headers, footers,  -and sidebars. -▇▇ -Docking a widget removes it from the layout and  -fixes its position, aligned to either the top,  -right, bottom, or left edges of a container. - -Docked widgets will not scroll out of view,  -making them ideal for sticky headers, footers,  -and sidebars. - -Docking a widget removes it from the layout and  -fixes its position, aligned to either the top,  -right, bottom, or left edges of a container. - -Docked widgets will not scroll out of view,  -making them ideal for sticky headers, footers,  -and sidebars. - - - - -
- Historical snapshot -
-
-
-
-
-
-
- -
-
-
-
-
- - test_center_layout - - (99.90% source similarity) -
- /Users/darrenburns/Code/textual/tests/snapshot_tests/test_snapshots.py:17 -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Textual - - - - - - - - - - - -One - - - -Two - - -Three - - - - - - - - - - - - - - - - - - -
- Output from test -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Textual - - - - - - - - - - -One - - - -Two - - -Three - - - - - - - - - - - - - - - - - -
- Historical snapshot -
-
-
-
-
-
-
- -
-
-
-
-
- - test_layers - - (99.94% source similarity) -
- /Users/darrenburns/Code/textual/tests/snapshot_tests/test_snapshots.py:13 -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Textual - - - - - - - - - - - - - - - - - - - - -box1 (layer = above) - - - - - -box2 (layer = below) - - - - - - - - - - -
- Output from test -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Textual - - - - - - - - - - - - - - - - - - - -box1 (layer = above) - - - - - -box2 (layer = below) - - - - - - - - - -
- Historical snapshot -
-
-
-
-
-
-
- -
-
-
-
-
- - test_vertical_layout - - (99.96% source similarity) -
- /Users/darrenburns/Code/textual/tests/snapshot_tests/test_snapshots.py:25 -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Textual - - - - - - - - - - ┌──────────────────────────────────────────────────────────────────────────────┐ -One - - - - - -└──────────────────────────────────────────────────────────────────────────────┘ -┌──────────────────────────────────────────────────────────────────────────────┐ -Two - - - - - -└──────────────────────────────────────────────────────────────────────────────┘ -┌──────────────────────────────────────────────────────────────────────────────┐ -Three - - - - - -└──────────────────────────────────────────────────────────────────────────────┘ - - - - -
- Output from test -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Textual - - - - - - - - - ┌──────────────────────────────────────────────────────────────────────────────┐ -One - - - - - -└──────────────────────────────────────────────────────────────────────────────┘ -┌──────────────────────────────────────────────────────────────────────────────┐ -Two - - - - - -└──────────────────────────────────────────────────────────────────────────────┘ -┌──────────────────────────────────────────────────────────────────────────────┐ -Three - - - - - -└──────────────────────────────────────────────────────────────────────────────┘ - - - -
- Historical snapshot -
-
-
-
-
-
-
- -
-
-
-
-
- - test_grid_layout_basic_overflow - - (99.98% source similarity) -
- /Users/darrenburns/Code/textual/tests/snapshot_tests/test_snapshots.py:5 -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Textual - - - - - - - - - - ┌────────────────────────┐┌─────────────────────────┐┌─────────────────────────┐ -OneeeeTwoThree - - - - - -└────────────────────────┘└─────────────────────────┘└─────────────────────────┘ -┌────────────────────────┐┌─────────────────────────┐┌─────────────────────────┐ -FourFiveSix - - - - - -└────────────────────────┘└─────────────────────────┘└─────────────────────────┘ -┌────────────────────────┐ -Seven - - - - - -└────────────────────────┘ - - - - -
- Output from test -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Textual - - - - - - - - - ┌────────────────────────┐┌─────────────────────────┐┌─────────────────────────┐ -OneeeeTwoThree - - - - - -└────────────────────────┘└─────────────────────────┘└─────────────────────────┘ -┌────────────────────────┐┌─────────────────────────┐┌─────────────────────────┐ -FourFiveSix - - - - - -└────────────────────────┘└─────────────────────────┘└─────────────────────────┘ -┌────────────────────────┐ -Seven - - - - - -└────────────────────────┘ - - - -
- Historical snapshot -
-
-
-
-
-
-
- -
-
-
-
-
- - test_grid_layout_basic - - (99.98% source similarity) -
- /Users/darrenburns/Code/textual/tests/snapshot_tests/test_snapshots.py:1 -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Textual - - - - - - - - - - ┌────────────────────────┐┌─────────────────────────┐┌─────────────────────────┐ -OneTwoThree - - - - - - - - - -└────────────────────────┘└─────────────────────────┘└─────────────────────────┘ -┌────────────────────────┐┌─────────────────────────┐┌─────────────────────────┐ -FourFiveSix - - - - - - - - - -└────────────────────────┘└─────────────────────────┘└─────────────────────────┘ - - - - -
- Output from test -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Textual - - - - - - - - - ┌────────────────────────┐┌─────────────────────────┐┌─────────────────────────┐ -OneTwoThree - - - - - - - - - -└────────────────────────┘└─────────────────────────┘└─────────────────────────┘ -┌────────────────────────┐┌─────────────────────────┐┌─────────────────────────┐ -FourFiveSix - - - - - - - - - -└────────────────────────┘└─────────────────────────┘└─────────────────────────┘ - - - -
- Historical snapshot -
-
-
-
-
-
-
- -
-
-
-
-
- - test_horizontal_layout - - (99.98% source similarity) -
- /Users/darrenburns/Code/textual/tests/snapshot_tests/test_snapshots.py:21 -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Textual - - - - - - - - - - ┌────────────────────────┐┌─────────────────────────┐┌─────────────────────────┐ -OneTwoThree - - - - - - - - - - - - - - - - - - - - - -└────────────────────────┘└─────────────────────────┘└─────────────────────────┘ - - - - -
- Output from test -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Textual - - - - - - - - - ┌────────────────────────┐┌─────────────────────────┐┌─────────────────────────┐ -OneTwoThree - - - - - - - - - - - - - - - - - - - - - -└────────────────────────┘└─────────────────────────┘└─────────────────────────┘ - - - -
- Historical snapshot -
-
-
-
-
-
-
- - -
-
-
-
-

If you're happy with the change, run pytest with the --snapshot-update flag to update the snapshot.

-
-
-
-
- -
-
-
-

Report generated at UTC 2022-09-20 13:52:10.785737.

-
-
-
- -
- - - \ No newline at end of file From e3a917ea71dd59e91971fc416bc8d9fee35299a1 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 20 Sep 2022 15:25:04 +0100 Subject: [PATCH 12/46] Fix guide example --- docs/examples/guide/layout/grid_layout2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/guide/layout/grid_layout2.py b/docs/examples/guide/layout/grid_layout2.py index 1df6b5938..fa0094073 100644 --- a/docs/examples/guide/layout/grid_layout2.py +++ b/docs/examples/guide/layout/grid_layout2.py @@ -6,7 +6,7 @@ class GridLayoutExample(App): CSS_PATH = "grid_layout1.css" def compose(self) -> ComposeResult: - yield Static("Oneeee", classes="box") + yield Static("One", classes="box") yield Static("Two", classes="box") yield Static("Three", classes="box") yield Static("Four", classes="box") From 5e1c47b990e1aba416bb5b183af865ade7a7c51b Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 20 Sep 2022 15:25:35 +0100 Subject: [PATCH 13/46] Remove unused pytest marker declaration --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e03c8828c..e1c73cfa3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,6 @@ 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] From 29da6badfda9faa9a62773dcc3fa1fa1e7003922 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 20 Sep 2022 15:29:42 +0100 Subject: [PATCH 14/46] Use type hints and add docstring to snap_compare --- tests/snapshot_tests/conftest.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/snapshot_tests/conftest.py b/tests/snapshot_tests/conftest.py index c9e7c86eb..d2508d046 100644 --- a/tests/snapshot_tests/conftest.py +++ b/tests/snapshot_tests/conftest.py @@ -15,6 +15,7 @@ from _pytest.terminal import TerminalReporter from jinja2 import Template from rich.console import Console from rich.panel import Panel +from syrupy import SnapshotAssertion from textual._doc import take_svg_screenshot @@ -24,8 +25,13 @@ snapshot_pass = pytest.StashKey[bool]() @pytest.fixture -def snap_compare(snapshot, request: FixtureRequest) -> Callable[[str], bool]: - def compare(app_path: str, snapshot) -> bool: +def snap_compare(snapshot: SnapshotAssertion, request: FixtureRequest) -> Callable[[str], bool]: + """ + This fixture returns a function which can be used to compare the output of a Textual + app with the output of the same app in the past. This is snapshot testing, and it + used to catch regressions in output. + """ + def compare(app_path: str, snapshot: SnapshotAssertion) -> bool: node = request.node actual_screenshot = take_svg_screenshot(app_path) result = snapshot == actual_screenshot From 2bed87f2ee3eb484f8279cdab71e81d8b60e1834 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 20 Sep 2022 15:31:48 +0100 Subject: [PATCH 15/46] Run black on snapshot_tests/conftest --- tests/snapshot_tests/conftest.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/tests/snapshot_tests/conftest.py b/tests/snapshot_tests/conftest.py index d2508d046..8b18bea5d 100644 --- a/tests/snapshot_tests/conftest.py +++ b/tests/snapshot_tests/conftest.py @@ -25,12 +25,15 @@ snapshot_pass = pytest.StashKey[bool]() @pytest.fixture -def snap_compare(snapshot: SnapshotAssertion, request: FixtureRequest) -> Callable[[str], bool]: +def snap_compare( + snapshot: SnapshotAssertion, request: FixtureRequest +) -> Callable[[str], bool]: """ This fixture returns a function which can be used to compare the output of a Textual app with the output of the same app in the past. This is snapshot testing, and it used to catch regressions in output. """ + def compare(app_path: str, snapshot: SnapshotAssertion) -> bool: node = request.node actual_screenshot = take_svg_screenshot(app_path) @@ -79,9 +82,10 @@ def pytest_sessionfinish( SvgSnapshotDiff( snapshot=str(snapshot_svg), actual=str(actual_svg), - file_similarity=100 * difflib.SequenceMatcher(a=str(snapshot_svg), - b=str( - actual_svg)).ratio(), + file_similarity=100 + * difflib.SequenceMatcher( + a=str(snapshot_svg), b=str(actual_svg) + ).ratio(), test_name=name, path=path, line_number=line_index + 1, @@ -93,7 +97,9 @@ def pytest_sessionfinish( diffs = list(reversed(sorted(diffs, key=diff_sort_key))) conftest_path = Path(__file__) - snapshot_template_path = conftest_path.parent / "snapshot_report_template.jinja2" + snapshot_template_path = ( + conftest_path.parent / "snapshot_report_template.jinja2" + ) snapshot_report_path = conftest_path.parent / "output/snapshot_report.html" template = Template(snapshot_template_path.read_text()) @@ -108,7 +114,7 @@ def pytest_sessionfinish( pass_percentage=100 * (num_snapshots_passing / max(num_snapshot_tests, 1)), fail_percentage=100 * (num_fails / max(num_snapshot_tests, 1)), num_snapshot_tests=num_snapshot_tests, - now=datetime.utcnow() + now=datetime.utcnow(), ) with open(snapshot_report_path, "wt") as snapshot_file: snapshot_file.write(rendered_report) @@ -137,5 +143,7 @@ def pytest_terminal_summary( snapshot_report_location = config._textual_snapshot_html_report 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) + title="[b red]Textual Snapshot Test Summary", + padding=1, + ) console.print(summary_panel) From 0ce222d25979d133e7380872720614a2cc77df9c Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 20 Sep 2022 15:34:15 +0100 Subject: [PATCH 16/46] Remove redundant info from docstring --- tests/snapshot_tests/conftest.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/snapshot_tests/conftest.py b/tests/snapshot_tests/conftest.py index 8b18bea5d..2332db2c3 100644 --- a/tests/snapshot_tests/conftest.py +++ b/tests/snapshot_tests/conftest.py @@ -133,9 +133,6 @@ def pytest_terminal_summary( :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 = getattr(config, "_textual_snapshots", None) console = Console() From c54c844695b7749e88d3904d1995fda866c0a6c9 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 20 Sep 2022 15:42:29 +0100 Subject: [PATCH 17/46] Update usages of approx --- tests/test_animator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_animator.py b/tests/test_animator.py index 159600d7a..a92f38c96 100644 --- a/tests/test_animator.py +++ b/tests/test_animator.py @@ -60,7 +60,7 @@ def test_simple_animation(): assert animate_test.foo == 40.0 assert animation(time + 2.9) is False # Not quite final value - assert pytest.approx(animate_test.foo, 49.0) + assert animate_test.foo == pytest.approx(49.0) assert animation(time + 3.0) is True # True to indicate animation is complete assert animate_test.foo is None # This is final_value @@ -161,7 +161,7 @@ def test_animatable(): assert animate_test.bar.value == 40.0 assert animation(time + 2.9) is False - assert pytest.approx(animate_test.bar.value, 49.0) + assert animate_test.bar.value == pytest.approx(49.0) assert animation(time + 3.0) is True # True to indicate animation is complete assert animate_test.bar.value == 50.0 From d2f8ad5257b5f7ddb0984f4a56b81f69758793c8 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 20 Sep 2022 15:46:08 +0100 Subject: [PATCH 18/46] Add Makefile target for snapshot update --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a4aab88a8..429996c68 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,8 @@ test: pytest --cov-report term-missing --cov=textual tests/ -vv unit-test: pytest --cov-report term-missing --cov=textual tests/ -vv -m "not integration_test" +test-snapshot-update: + pytest --cov-report term-missing --cov=textual tests/ -vv --snapshot-update typecheck: mypy src/textual format: @@ -14,4 +16,3 @@ docs-build: mkdocs build docs-deploy: mkdocs gh-deploy - From 7e72cf1d281101a3f3be0acad1356c06e82895eb Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 20 Sep 2022 15:46:43 +0100 Subject: [PATCH 19/46] Update snapshots --- .../__snapshots__/test_snapshots.ambr | 114 +++++++++--------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 38cf4eb34..ea453a22a 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -489,133 +489,133 @@ font-weight: 700; } - .terminal-1607812088-matrix { + .terminal-498287401-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-1607812088-title { + .terminal-498287401-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-1607812088-r1 { fill: #008000 } - .terminal-1607812088-r2 { fill: #c5c8c6 } - .terminal-1607812088-r3 { fill: #f3f3f3 } + .terminal-498287401-r1 { fill: #008000 } + .terminal-498287401-r2 { fill: #c5c8c6 } + .terminal-498287401-r3 { fill: #f3f3f3 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - Textual + Textual - - - - ┌────────────────────────┐┌─────────────────────────┐┌─────────────────────────┐ - OneeeeTwoThree - - - - - - └────────────────────────┘└─────────────────────────┘└─────────────────────────┘ - ┌────────────────────────┐┌─────────────────────────┐┌─────────────────────────┐ - FourFiveSix - - - - - - └────────────────────────┘└─────────────────────────┘└─────────────────────────┘ - ┌────────────────────────┐ - Seven - - - - - - └────────────────────────┘ + + + + ┌────────────────────────┐┌─────────────────────────┐┌─────────────────────────┐ + OneTwoThree + + + + + + └────────────────────────┘└─────────────────────────┘└─────────────────────────┘ + ┌────────────────────────┐┌─────────────────────────┐┌─────────────────────────┐ + FourFiveSix + + + + + + └────────────────────────┘└─────────────────────────┘└─────────────────────────┘ + ┌────────────────────────┐ + Seven + + + + + + └────────────────────────┘ From 26fb89005ca19a5023070cafd0441bba2261f518 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 20 Sep 2022 16:44:18 +0100 Subject: [PATCH 20/46] Skip artifact creation/upload on Windows --- .github/workflows/pythonpackage.yml | 5 +++++ tests/snapshot_tests/conftest.py | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 7e7d6fabc..cedb14bbb 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -43,3 +43,8 @@ jobs: run: | source $VENV python e2e_tests/sandbox_basic_test.py basic 2.0 + - name: Upload snapshot report + uses: actions/upload-artifact@v3 + with: + name: snapshot-report-textual + path: tests/snapshot_tests/output/snapshot_report.html diff --git a/tests/snapshot_tests/conftest.py b/tests/snapshot_tests/conftest.py index 2332db2c3..92871e0bc 100644 --- a/tests/snapshot_tests/conftest.py +++ b/tests/snapshot_tests/conftest.py @@ -1,4 +1,6 @@ import difflib +import os +import sys from dataclasses import dataclass from datetime import datetime from functools import partial @@ -92,7 +94,9 @@ def pytest_sessionfinish( ) ) - if diffs: + # TODO: Skipping writing artifacts on Windows on CI for now + is_windows_ci = sys.platform == "win32" and os.getenv("CI") is not None + if diffs and not is_windows_ci: diff_sort_key = attrgetter("file_similarity") diffs = list(reversed(sorted(diffs, key=diff_sort_key))) From 8e09c0271abc7ce2b4c829b18d275854794dc970 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 20 Sep 2022 16:44:32 +0100 Subject: [PATCH 21/46] Adding windows snapshot testing conditional on github actions --- .github/workflows/pythonpackage.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index cedb14bbb..28dd16d13 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -44,6 +44,7 @@ jobs: source $VENV python e2e_tests/sandbox_basic_test.py basic 2.0 - name: Upload snapshot report + if: matrix.os != 'windows-latest' uses: actions/upload-artifact@v3 with: name: snapshot-report-textual From 17f2e3e322a84da1354f524be810f01cbd23cfa2 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 20 Sep 2022 16:53:23 +0100 Subject: [PATCH 22/46] Intentionally trigger test failure to test artifact upload --- docs/examples/guide/layout/center_layout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/guide/layout/center_layout.py b/docs/examples/guide/layout/center_layout.py index 0cf31c615..204242117 100644 --- a/docs/examples/guide/layout/center_layout.py +++ b/docs/examples/guide/layout/center_layout.py @@ -6,7 +6,7 @@ class CenterLayoutExample(App): CSS_PATH = "center_layout.css" def compose(self) -> ComposeResult: - yield Static("One", id="bottom") + yield Static("One!", id="bottom") yield Static("Two", id="middle") yield Static("Three", id="top") From da05b66666d2c57e8219095b5710258518c09797 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 20 Sep 2022 16:54:59 +0100 Subject: [PATCH 23/46] Intentionally trigger test failure to test artifact upload --- docs/examples/guide/layout/center_layout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/guide/layout/center_layout.py b/docs/examples/guide/layout/center_layout.py index 204242117..0cf31c615 100644 --- a/docs/examples/guide/layout/center_layout.py +++ b/docs/examples/guide/layout/center_layout.py @@ -6,7 +6,7 @@ class CenterLayoutExample(App): CSS_PATH = "center_layout.css" def compose(self) -> ComposeResult: - yield Static("One!", id="bottom") + yield Static("One", id="bottom") yield Static("Two", id="middle") yield Static("Three", id="top") From 9606d43897585dfdbe50e3551bb5c6b0942d08fe Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 20 Sep 2022 16:59:32 +0100 Subject: [PATCH 24/46] Intentionally trigger test failure to test artifact upload --- docs/examples/guide/layout/center_layout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/guide/layout/center_layout.py b/docs/examples/guide/layout/center_layout.py index 0cf31c615..204242117 100644 --- a/docs/examples/guide/layout/center_layout.py +++ b/docs/examples/guide/layout/center_layout.py @@ -6,7 +6,7 @@ class CenterLayoutExample(App): CSS_PATH = "center_layout.css" def compose(self) -> ComposeResult: - yield Static("One", id="bottom") + yield Static("One!", id="bottom") yield Static("Two", id="middle") yield Static("Three", id="top") From 49abcbadfe053c5d96fc0c5ebbcb78e29369fd48 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 20 Sep 2022 17:05:43 +0100 Subject: [PATCH 25/46] Upload snapshot report when build fails --- .github/workflows/pythonpackage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 28dd16d13..b9edee5e0 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -44,7 +44,7 @@ jobs: source $VENV python e2e_tests/sandbox_basic_test.py basic 2.0 - name: Upload snapshot report - if: matrix.os != 'windows-latest' + if: always() uses: actions/upload-artifact@v3 with: name: snapshot-report-textual From 56b7ec70b8eede3f0b865af11bb0d9f04237f5e0 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 20 Sep 2022 17:12:52 +0100 Subject: [PATCH 26/46] Change write mode --- tests/snapshot_tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/snapshot_tests/conftest.py b/tests/snapshot_tests/conftest.py index 92871e0bc..76cf997a0 100644 --- a/tests/snapshot_tests/conftest.py +++ b/tests/snapshot_tests/conftest.py @@ -120,7 +120,7 @@ def pytest_sessionfinish( num_snapshot_tests=num_snapshot_tests, now=datetime.utcnow(), ) - with open(snapshot_report_path, "wt") as snapshot_file: + with open(snapshot_report_path, "w+") as snapshot_file: snapshot_file.write(rendered_report) session.config._textual_snapshots = diffs From 6a63ffbff9cff89b5655f3896026b971f41a58f7 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 20 Sep 2022 17:34:50 +0100 Subject: [PATCH 27/46] Make output dir if not exists --- tests/snapshot_tests/conftest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/snapshot_tests/conftest.py b/tests/snapshot_tests/conftest.py index 76cf997a0..c49673bfb 100644 --- a/tests/snapshot_tests/conftest.py +++ b/tests/snapshot_tests/conftest.py @@ -104,7 +104,9 @@ def pytest_sessionfinish( snapshot_template_path = ( conftest_path.parent / "snapshot_report_template.jinja2" ) - snapshot_report_path = conftest_path.parent / "output/snapshot_report.html" + snapshot_report_path_dir = conftest_path.parent / "output" + snapshot_report_path_dir.mkdir(parents=True, exist_ok=True) + snapshot_report_path = snapshot_report_path_dir / "snapshot_report.html" template = Template(snapshot_template_path.read_text()) From 4a46257e383dc6cea5167de7c89667293dda1b4d Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Tue, 20 Sep 2022 17:46:41 +0100 Subject: [PATCH 28/46] Revert center layout snapshot to make it pass --- docs/examples/guide/layout/center_layout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/guide/layout/center_layout.py b/docs/examples/guide/layout/center_layout.py index 204242117..0cf31c615 100644 --- a/docs/examples/guide/layout/center_layout.py +++ b/docs/examples/guide/layout/center_layout.py @@ -6,7 +6,7 @@ class CenterLayoutExample(App): CSS_PATH = "center_layout.css" def compose(self) -> ComposeResult: - yield Static("One!", id="bottom") + yield Static("One", id="bottom") yield Static("Two", id="middle") yield Static("Three", id="top") From 1a658955daa0a73c29b95152541d74b8e294b393 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 21 Sep 2022 11:37:16 +0100 Subject: [PATCH 29/46] Reenable artifacts on windows --- tests/snapshot_tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/snapshot_tests/conftest.py b/tests/snapshot_tests/conftest.py index c49673bfb..92a84374c 100644 --- a/tests/snapshot_tests/conftest.py +++ b/tests/snapshot_tests/conftest.py @@ -95,8 +95,8 @@ def pytest_sessionfinish( ) # TODO: Skipping writing artifacts on Windows on CI for now - is_windows_ci = sys.platform == "win32" and os.getenv("CI") is not None - if diffs and not is_windows_ci: + # is_windows_ci = sys.platform == "win32" and os.getenv("CI") is not None + if diffs: diff_sort_key = attrgetter("file_similarity") diffs = list(reversed(sorted(diffs, key=diff_sort_key))) From 644922c8294de0c7c1cb98f14740b4544aeb59c8 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 21 Sep 2022 11:45:16 +0100 Subject: [PATCH 30/46] Write snapshot file as utf-8 --- tests/snapshot_tests/conftest.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/snapshot_tests/conftest.py b/tests/snapshot_tests/conftest.py index 92a84374c..991c0e33b 100644 --- a/tests/snapshot_tests/conftest.py +++ b/tests/snapshot_tests/conftest.py @@ -1,6 +1,4 @@ import difflib -import os -import sys from dataclasses import dataclass from datetime import datetime from functools import partial @@ -122,7 +120,7 @@ def pytest_sessionfinish( num_snapshot_tests=num_snapshot_tests, now=datetime.utcnow(), ) - with open(snapshot_report_path, "w+") as snapshot_file: + with open(snapshot_report_path, "w+", encoding="utf-8") as snapshot_file: snapshot_file.write(rendered_report) session.config._textual_snapshots = diffs From 53ac7b4fa48fc60afbd1f517d75d6405bf441cc8 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 21 Sep 2022 11:53:45 +0100 Subject: [PATCH 31/46] Force legacy windows False on the app --- src/textual/_doc.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/textual/_doc.py b/src/textual/_doc.py index 0d8e9f9ec..52f9ec97c 100644 --- a/src/textual/_doc.py +++ b/src/textual/_doc.py @@ -1,16 +1,10 @@ from __future__ import annotations import os -import runpy import shlex -from typing import cast, TYPE_CHECKING, Iterable -from typing import TYPE_CHECKING, cast - -from textual._import_app import AppFail, import_app - -if TYPE_CHECKING: - from textual.app import App +from typing import Iterable +from textual._import_app import import_app # This module defines our "Custom Fences", powered by SuperFences # @link https://facelessuser.github.io/pymdown-extensions/extensions/superfences/#custom-fences @@ -55,6 +49,7 @@ def take_svg_screenshot( os.environ["COLUMNS"] = str(columns) os.environ["LINES"] = str(rows) app = import_app(app_path) + app.console.legacy_windows = False if title is None: title = app.title From 04a513ff9fd571daf3db326f8c530f78c5cef92d Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 21 Sep 2022 15:18:53 +0100 Subject: [PATCH 32/46] Add debug info --- docs/examples/guide/layout/center_layout.py | 2 +- src/textual/_doc.py | 15 +++- tests/snapshot_tests/conftest.py | 40 ++++++--- .../snapshot_report_template.jinja2 | 86 +++++++++++++++++-- 4 files changed, 118 insertions(+), 25 deletions(-) diff --git a/docs/examples/guide/layout/center_layout.py b/docs/examples/guide/layout/center_layout.py index 0cf31c615..463456f4b 100644 --- a/docs/examples/guide/layout/center_layout.py +++ b/docs/examples/guide/layout/center_layout.py @@ -6,7 +6,7 @@ class CenterLayoutExample(App): CSS_PATH = "center_layout.css" def compose(self) -> ComposeResult: - yield Static("One", id="bottom") + yield Static("Onee", id="bottom") yield Static("Two", id="middle") yield Static("Three", id="top") diff --git a/src/textual/_doc.py b/src/textual/_doc.py index 52f9ec97c..9f4bd673d 100644 --- a/src/textual/_doc.py +++ b/src/textual/_doc.py @@ -4,6 +4,7 @@ import os import shlex from typing import Iterable +from textual.app import App from textual._import_app import import_app # This module defines our "Custom Fences", powered by SuperFences @@ -25,7 +26,9 @@ def format_svg(source, language, css_class, options, md, attrs, **kwargs) -> str try: rows = int(attrs.get("lines", 24)) columns = int(attrs.get("columns", 80)) - svg = take_svg_screenshot(path, press, title, terminal_size=(rows, columns)) + svg = take_svg_screenshot( + None, path, press, title, terminal_size=(rows, columns) + ) finally: os.chdir(cwd) @@ -39,8 +42,9 @@ def format_svg(source, language, css_class, options, md, attrs, **kwargs) -> str def take_svg_screenshot( - app_path: str, - press: Iterable[str] = ("_", "_"), + app: App | None = None, + app_path: str | None = None, + press: Iterable[str] = ("_",), title: str | None = None, terminal_size: tuple[int, int] = (24, 80), ) -> str: @@ -48,7 +52,10 @@ def take_svg_screenshot( os.environ["COLUMNS"] = str(columns) os.environ["LINES"] = str(rows) - app = import_app(app_path) + + if app is None: + app = import_app(app_path) + app.console.legacy_windows = False if title is None: title = app.title diff --git a/tests/snapshot_tests/conftest.py b/tests/snapshot_tests/conftest.py index 991c0e33b..5617fbc3f 100644 --- a/tests/snapshot_tests/conftest.py +++ b/tests/snapshot_tests/conftest.py @@ -1,4 +1,6 @@ import difflib +import os +import pprint from dataclasses import dataclass from datetime import datetime from functools import partial @@ -13,15 +15,19 @@ from _pytest.fixtures import FixtureRequest from _pytest.main import Session from _pytest.terminal import TerminalReporter from jinja2 import Template +from rich import inspect from rich.console import Console from rich.panel import Panel from syrupy import SnapshotAssertion +from textual.app import App from textual._doc import take_svg_screenshot +from textual._import_app import import_app -snapshot_svg_key = pytest.StashKey[str]() -actual_svg_key = pytest.StashKey[str]() -snapshot_pass = pytest.StashKey[bool]() +TEXTUAL_SNAPSHOT_SVG_KEY = pytest.StashKey[str]() +TEXTUAL_ACTUAL_SVG_KEY = pytest.StashKey[str]() +TEXTUAL_SNAPSHOT_PASS = pytest.StashKey[bool]() +TEXTUAL_APP_KEY = pytest.StashKey[App]() @pytest.fixture @@ -36,15 +42,17 @@ def snap_compare( def compare(app_path: str, snapshot: SnapshotAssertion) -> bool: node = request.node - actual_screenshot = take_svg_screenshot(app_path) + app = import_app(app_path) + actual_screenshot = take_svg_screenshot(app=app) 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 + node.stash[TEXTUAL_SNAPSHOT_SVG_KEY] = "\n".join(str(snapshot).splitlines()[1:-1]) + node.stash[TEXTUAL_ACTUAL_SVG_KEY] = actual_screenshot + node.stash[TEXTUAL_APP_KEY] = app else: - node.stash[snapshot_pass] = True + node.stash[TEXTUAL_SNAPSHOT_PASS] = True return result @@ -59,6 +67,8 @@ class SvgSnapshotDiff: file_similarity: float path: PathLike line_number: int + app: App + environment: dict def pytest_sessionfinish( @@ -73,22 +83,28 @@ def pytest_sessionfinish( 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: + + # Grab the data our fixture attached to the pytest node + num_snapshots_passing += int(item.stash.get(TEXTUAL_SNAPSHOT_PASS, False)) + snapshot_svg = item.stash.get(TEXTUAL_SNAPSHOT_SVG_KEY, None) + actual_svg = item.stash.get(TEXTUAL_ACTUAL_SVG_KEY, None) + app = item.stash.get(TEXTUAL_APP_KEY, None) + + if snapshot_svg and actual_svg and app: path, line_index, name = item.reportinfo() diffs.append( SvgSnapshotDiff( snapshot=str(snapshot_svg), actual=str(actual_svg), file_similarity=100 - * difflib.SequenceMatcher( + * difflib.SequenceMatcher( a=str(snapshot_svg), b=str(actual_svg) ).ratio(), test_name=name, path=path, line_number=line_index + 1, + app=app, + environment=dict(os.environ), ) ) diff --git a/tests/snapshot_tests/snapshot_report_template.jinja2 b/tests/snapshot_tests/snapshot_report_template.jinja2 index 9427a9606..917d798dd 100644 --- a/tests/snapshot_tests/snapshot_report_template.jinja2 +++ b/tests/snapshot_tests/snapshot_report_template.jinja2 @@ -8,6 +8,8 @@ integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT" crossorigin="anonymous"> + +
@@ -51,14 +53,18 @@ ({{ "%.2f"|format(diff.file_similarity) }}% source similarity)
- {{ diff.path }}:{{ diff.line_number }} +
+ {{ diff.path }}:{{ diff.line_number }} +
{{ diff.actual }}
- Output from test + Output from test (More info)
@@ -69,6 +75,63 @@
+
@@ -78,21 +141,28 @@
-

If you're happy with the change, run pytest with the --snapshot-update flag to update the snapshot.

+

If you're happy with the test output, run pytest with the --snapshot-update flag to update the snapshot. +

-
-
-
-

Report generated at UTC {{ now }}.

+
+
+
+

Report generated at UTC {{ now }}.

+
-
+ + + From f88a13fa024af02837d79ae9b0d9de3fc4cbfaf3 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 21 Sep 2022 15:19:13 +0100 Subject: [PATCH 33/46] Fix snapshot --- docs/examples/guide/layout/center_layout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/guide/layout/center_layout.py b/docs/examples/guide/layout/center_layout.py index 463456f4b..0cf31c615 100644 --- a/docs/examples/guide/layout/center_layout.py +++ b/docs/examples/guide/layout/center_layout.py @@ -6,7 +6,7 @@ class CenterLayoutExample(App): CSS_PATH = "center_layout.css" def compose(self) -> ComposeResult: - yield Static("Onee", id="bottom") + yield Static("One", id="bottom") yield Static("Two", id="middle") yield Static("Three", id="top") From 8db7a07eef8a89d66cb4d2ef1e687713d31cb29c Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 21 Sep 2022 15:35:16 +0100 Subject: [PATCH 34/46] Force legacy_windows to False in Textual --- src/textual/_doc.py | 1 - src/textual/app.py | 1 + tests/snapshot_tests/conftest.py | 4 +--- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/textual/_doc.py b/src/textual/_doc.py index 9f4bd673d..990073e4a 100644 --- a/src/textual/_doc.py +++ b/src/textual/_doc.py @@ -56,7 +56,6 @@ def take_svg_screenshot( if app is None: app = import_app(app_path) - app.console.legacy_windows = False if title is None: title = app.title diff --git a/src/textual/app.py b/src/textual/app.py index be5b7b938..ca6f55a42 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -176,6 +176,7 @@ class App(Generic[ReturnType], DOMNode): markup=False, highlight=False, emoji=False, + legacy_windows=False, ) self.error_console = Console(markup=False, stderr=True) self.driver_class = driver_class or self.get_driver_class() diff --git a/tests/snapshot_tests/conftest.py b/tests/snapshot_tests/conftest.py index 5617fbc3f..514a01621 100644 --- a/tests/snapshot_tests/conftest.py +++ b/tests/snapshot_tests/conftest.py @@ -1,6 +1,5 @@ import difflib import os -import pprint from dataclasses import dataclass from datetime import datetime from functools import partial @@ -15,14 +14,13 @@ from _pytest.fixtures import FixtureRequest from _pytest.main import Session from _pytest.terminal import TerminalReporter from jinja2 import Template -from rich import inspect from rich.console import Console from rich.panel import Panel from syrupy import SnapshotAssertion -from textual.app import App from textual._doc import take_svg_screenshot from textual._import_app import import_app +from textual.app import App TEXTUAL_SNAPSHOT_SVG_KEY = pytest.StashKey[str]() TEXTUAL_ACTUAL_SVG_KEY = pytest.StashKey[str]() From cbe91fdd667b3ce6ff37eb6611514859b7c5a13f Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 22 Sep 2022 10:55:09 +0100 Subject: [PATCH 35/46] Ensure legacy_windows is False on Console instance which exports screenshot --- src/textual/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/textual/app.py b/src/textual/app.py index ca6f55a42..04a3c79ca 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -558,6 +558,7 @@ class App(Generic[ReturnType], DOMNode): force_terminal=True, color_system="truecolor", record=True, + legacy_windows=False, ) screen_render = self.screen._compositor.render(full=True) console.print(screen_render) From b5540caa33d6370f8df4b0957d8bea18e422a8b5 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 22 Sep 2022 13:48:17 +0100 Subject: [PATCH 36/46] Add toggle for overlay mode --- tests/snapshot_tests/conftest.py | 13 +- .../snapshot_report_template.jinja2 | 159 +++++++++++------- 2 files changed, 103 insertions(+), 69 deletions(-) diff --git a/tests/snapshot_tests/conftest.py b/tests/snapshot_tests/conftest.py index 514a01621..2809290d2 100644 --- a/tests/snapshot_tests/conftest.py +++ b/tests/snapshot_tests/conftest.py @@ -110,7 +110,7 @@ def pytest_sessionfinish( # is_windows_ci = sys.platform == "win32" and os.getenv("CI") is not None if diffs: diff_sort_key = attrgetter("file_similarity") - diffs = list(reversed(sorted(diffs, key=diff_sort_key))) + diffs = sorted(diffs, key=diff_sort_key) conftest_path = Path(__file__) snapshot_template_path = ( @@ -156,9 +156,8 @@ def pytest_terminal_summary( console = Console() if diffs: snapshot_report_location = config._textual_snapshot_html_report - 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) + console.rule("[b red]Textual Snapshot Report", style="red") + console.print(f"\n[black on red]{len(diffs)} mismatched snapshots[/]\n" + f"\n[b]View the [link=file://{snapshot_report_location}]failure report[/].\n") + console.print(f"[dim]{snapshot_report_location}\n") + console.rule(style="red") diff --git a/tests/snapshot_tests/snapshot_report_template.jinja2 b/tests/snapshot_tests/snapshot_report_template.jinja2 index 917d798dd..d5eb310fb 100644 --- a/tests/snapshot_tests/snapshot_report_template.jinja2 +++ b/tests/snapshot_tests/snapshot_report_template.jinja2 @@ -6,10 +6,24 @@ Textual Snapshot Test Report + -
@@ -47,14 +61,18 @@
-
- - {{ diff.test_name }} - - ({{ "%.2f"|format(diff.file_similarity) }}% source similarity) -
-
- {{ diff.path }}:{{ diff.line_number }} + + {{ diff.test_name }} + + {{ diff.path }}:{{ diff.line_number }} + + +
+ +
@@ -68,67 +86,77 @@
- {{ diff.snapshot }} +
+ +
+
+ {{ diff.snapshot }} +
Historical snapshot
- @@ -164,5 +192,12 @@ integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"> + + From ee0eed9bcb03d2eb23a5c7b46e2c7983f9590556 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 22 Sep 2022 13:49:17 +0100 Subject: [PATCH 37/46] Remove some unused CSS in snapshot report --- tests/snapshot_tests/snapshot_report_template.jinja2 | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/snapshot_tests/snapshot_report_template.jinja2 b/tests/snapshot_tests/snapshot_report_template.jinja2 index d5eb310fb..7bc41c9d7 100644 --- a/tests/snapshot_tests/snapshot_report_template.jinja2 +++ b/tests/snapshot_tests/snapshot_report_template.jinja2 @@ -10,10 +10,6 @@ .overlay-container { position: relative; } - - .diff-wrapper-snapshot { - } - .diff-wrapper-actual { mix-blend-mode: difference; position: absolute; From cc352faf63cc84ae20a4980bb119a6e4ee7f99d6 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 22 Sep 2022 13:56:20 +0100 Subject: [PATCH 38/46] Formatting --- tests/snapshot_tests/snapshot_report_template.jinja2 | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/snapshot_tests/snapshot_report_template.jinja2 b/tests/snapshot_tests/snapshot_report_template.jinja2 index 7bc41c9d7..2234d2e82 100644 --- a/tests/snapshot_tests/snapshot_report_template.jinja2 +++ b/tests/snapshot_tests/snapshot_report_template.jinja2 @@ -33,9 +33,7 @@ {{ diffs | length }} snapshots changed - - · - + · {{ passes }} snapshots matched From 9e2500aeca18f6fcbb8bbc9f05f1bd08e568bb7d Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 22 Sep 2022 14:15:52 +0100 Subject: [PATCH 39/46] Quick internal guide to snapshot testing --- notes/snapshot_testing.md | 43 +++++++++++++++++++ .../snapshot_report_template.jinja2 | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 notes/snapshot_testing.md diff --git a/notes/snapshot_testing.md b/notes/snapshot_testing.md new file mode 100644 index 000000000..651adfb10 --- /dev/null +++ b/notes/snapshot_testing.md @@ -0,0 +1,43 @@ +# Snapshot Testing + + +## What is snapshot testing? + +Some tests that run for Textual are snapshot tests. +When you first run a snapshot test, a screenshot of an app is taken and saved to disk. +Next time you run it, another screenshot is taken and compared with the original one. + +If the screenshots don't match, it means something has changed. +It's up to you to tell the test system whether that change is expected or not. + +This allows us to easily catch regressions in how Textual outputs to the terminal. + +Snapshot tests run alongside normal unit tests. + +## How do I write a snapshot test? + +1. Inject the `snap_compare` fixture into your test. +2. Pass in the path to the file which contains the Textual app. + +```python +def test_grid_layout_basic_overflow(snap_compare): + assert snap_compare("docs/examples/guide/layout/grid_layout2.py") +``` + +`snap_compare` can take additional arguments such as `press`, which allows +you to simulate key presses etc. +See the signature of `snap_compare` for more info. + +## A snapshot test failed, what do I do? + +When a snapshot test fails, a report will be created on your machine, and you +can use this report to visually compare the output from your test with the historical output for that test. + +This report will be visible at the bottom of the terminal after the `pytest` session completes, +or, if running in CI, it will be available as an artifact attached to the GitHub Actions summary. + +If you're happy that the new output of the app is correct, you can run `pytest` with the +`--snapshot-update` flag. This flag will update the snapshots for any test that is executed in the run, +so to update a snapshot for a single test, run only that test. + +With your snapshot on disk updated to match the new output, running the test again should result in a pass. diff --git a/tests/snapshot_tests/snapshot_report_template.jinja2 b/tests/snapshot_tests/snapshot_report_template.jinja2 index 2234d2e82..d1f7b2530 100644 --- a/tests/snapshot_tests/snapshot_report_template.jinja2 +++ b/tests/snapshot_tests/snapshot_report_template.jinja2 @@ -163,7 +163,7 @@
-

If you're happy with the test output, run pytest with the If you're happy with the test output, run pytest with the --snapshot-update flag to update the snapshot.

From 0c0f5cd3842103d419bff41e6e1045d13ebd7852 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 22 Sep 2022 14:32:25 +0100 Subject: [PATCH 40/46] Tidying up --- src/textual/_doc.py | 15 ++++++++++ tests/snapshot_tests/conftest.py | 48 ++++++++++++++++++++++---------- 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/src/textual/_doc.py b/src/textual/_doc.py index 990073e4a..17918e351 100644 --- a/src/textual/_doc.py +++ b/src/textual/_doc.py @@ -7,6 +7,7 @@ from typing import Iterable from textual.app import App from textual._import_app import 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: @@ -48,6 +49,20 @@ def take_svg_screenshot( title: str | None = None, terminal_size: tuple[int, int] = (24, 80), ) -> str: + """ + + Args: + app: An app instance. Must be supplied if app_path is not. + app_path: A path to an app. Must be supplied if app is not. + press: Key presses to run before taking screenshot. "_" is a short pause. + title: The terminal title in the output image. + terminal_size: A pair of integers (rows, columns), representing terminal size. + + Returns: + str: An SVG string, showing the content of the terminal window at the time + the screenshot was taken. + + """ rows, columns = terminal_size os.environ["COLUMNS"] = str(columns) diff --git a/tests/snapshot_tests/conftest.py b/tests/snapshot_tests/conftest.py index 2809290d2..79b5584b1 100644 --- a/tests/snapshot_tests/conftest.py +++ b/tests/snapshot_tests/conftest.py @@ -1,12 +1,13 @@ +from __future__ import annotations + import difflib import os 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, Callable +from typing import Union, List, Optional, Callable, Iterable import pytest from _pytest.config import ExitCode @@ -15,7 +16,6 @@ from _pytest.main import Session from _pytest.terminal import TerminalReporter from jinja2 import Template from rich.console import Console -from rich.panel import Panel from syrupy import SnapshotAssertion from textual._doc import take_svg_screenshot @@ -38,10 +38,32 @@ def snap_compare( used to catch regressions in output. """ - def compare(app_path: str, snapshot: SnapshotAssertion) -> bool: + def compare( + app_path: str, + press: Iterable[str] = ("_",), + terminal_size: tuple[int, int] = (24, 80), + ) -> bool: + """ + Compare a current screenshot of the app running at app_path, with + a previously accepted (validated by human) snapshot stored on disk. + When the `--snapshot-update` flag is supplied (provided by syrupy), + the snapshot on disk will be updated to match the current screenshot. + + Args: + app_path (str): The path of the app. + press (Iterable[str]): Key presses to run before taking screenshot. "_" is a short pause. + terminal_size (tuple[int, int]): A pair of integers (rows, columns), representing terminal size. + + Returns: + bool: True if the screenshot matches the snapshot. + """ node = request.node app = import_app(app_path) - actual_screenshot = take_svg_screenshot(app=app) + actual_screenshot = take_svg_screenshot( + app=app, + press=press, + terminal_size=terminal_size, + ) result = snapshot == actual_screenshot if result is False: @@ -54,11 +76,14 @@ def snap_compare( return result - return partial(compare, snapshot=snapshot) + return compare @dataclass class SvgSnapshotDiff: + """Model representing a diff between current screenshot of an app, + and the snapshot on disk. This is ultimately intended to be used in + a Jinja2 template.""" snapshot: Optional[str] actual: Optional[str] test_name: str @@ -74,9 +99,7 @@ def pytest_sessionfinish( exitstatus: Union[int, 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. + Generates the snapshot report and writes it to disk. """ diffs: List[SvgSnapshotDiff] = [] num_snapshots_passing = 0 @@ -106,8 +129,6 @@ def pytest_sessionfinish( ) ) - # TODO: Skipping writing artifacts on Windows on CI for now - # is_windows_ci = sys.platform == "win32" and os.getenv("CI") is not None if diffs: diff_sort_key = attrgetter("file_similarity") diffs = sorted(diffs, key=diff_sort_key) @@ -147,10 +168,7 @@ def pytest_terminal_summary( 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. + Displays the link to the snapshot report that was generated in a prior hook. """ diffs = getattr(config, "_textual_snapshots", None) console = Console() From d3dcce7aa49e8738c082772d8ad24f557d9576fc Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 22 Sep 2022 14:49:57 +0100 Subject: [PATCH 41/46] Update gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index a20548735..4580aac9a 100644 --- a/.gitignore +++ b/.gitignore @@ -113,3 +113,6 @@ venv.bak/ # mypy .mypy_cache/ + +# Snapshot testing report output directory +tests/snapshot_tests/output From e521eea99f9d8daa1b21ee302db32a68fda9c1f7 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 22 Sep 2022 14:54:37 +0100 Subject: [PATCH 42/46] Remove old sandbox files --- sandbox/README.md | 3 -- sandbox/align.css | 48 ------------------ sandbox/align.py | 30 ----------- sandbox/auto_test.css | 16 ------ sandbox/auto_test.py | 21 -------- sandbox/borders.py | 66 ------------------------ sandbox/buttons.css | 6 --- sandbox/buttons.py | 33 ------------ sandbox/color_names.py | 46 ----------------- sandbox/dev_sandbox.py | 38 -------------- sandbox/dev_sandbox.scss | 66 ------------------------ sandbox/fifty.py | 30 ----------- sandbox/horizontal.css | 18 ------- sandbox/horizontal.py | 26 ---------- sandbox/input.py | 67 ------------------------- sandbox/input.scss | 53 -------------------- sandbox/local_styles.css | 11 ---- sandbox/local_styles.py | 36 -------------- sandbox/nest.css | 25 ---------- sandbox/nest.py | 32 ------------ sandbox/scroll_to_widget.py | 72 --------------------------- sandbox/simplest.py | 3 -- sandbox/uber.css | 28 ----------- sandbox/uber.py | 83 ------------------------------- sandbox/vertical.css | 22 -------- sandbox/vertical.py | 29 ----------- sandbox/vertical_container.py | 94 ----------------------------------- 27 files changed, 1002 deletions(-) delete mode 100644 sandbox/README.md delete mode 100644 sandbox/align.css delete mode 100644 sandbox/align.py delete mode 100644 sandbox/auto_test.css delete mode 100644 sandbox/auto_test.py delete mode 100644 sandbox/borders.py delete mode 100644 sandbox/buttons.css delete mode 100644 sandbox/buttons.py delete mode 100644 sandbox/color_names.py delete mode 100644 sandbox/dev_sandbox.py delete mode 100644 sandbox/dev_sandbox.scss delete mode 100644 sandbox/fifty.py delete mode 100644 sandbox/horizontal.css delete mode 100644 sandbox/horizontal.py delete mode 100644 sandbox/input.py delete mode 100644 sandbox/input.scss delete mode 100644 sandbox/local_styles.css delete mode 100644 sandbox/local_styles.py delete mode 100644 sandbox/nest.css delete mode 100644 sandbox/nest.py delete mode 100644 sandbox/scroll_to_widget.py delete mode 100644 sandbox/simplest.py delete mode 100644 sandbox/uber.css delete mode 100644 sandbox/uber.py delete mode 100644 sandbox/vertical.css delete mode 100644 sandbox/vertical.py delete mode 100644 sandbox/vertical_container.py diff --git a/sandbox/README.md b/sandbox/README.md deleted file mode 100644 index bbc3b703d..000000000 --- a/sandbox/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Dev Sandbox - -This directory contains test code for Textual devs to experiment with new features. None of the .py files here are guaranteed to run or do anything useful, but you are welcome to look around. diff --git a/sandbox/align.css b/sandbox/align.css deleted file mode 100644 index 0ca179814..000000000 --- a/sandbox/align.css +++ /dev/null @@ -1,48 +0,0 @@ - - -Screen { - layout: vertical; - overflow: auto; -} - -Widget { - margin:1; -} - -#thing { - width: auto; - height: auto; - background:magenta; - margin: 1; - padding: 1; - border: solid white; - box-sizing: border-box; - border: solid white; - align-horizontal: center; -} - - -#thing2 { - border: solid white; - /* outline: heavy blue; */ - height: 10; - padding: 1 2; - - box-sizing: border-box; - - max-height: 100vh; - - background:green; - align-horizontal: center; - color:white; -} - - -#thing3 { - height: 10; - margin: 1; - background:blue; - color: white 50%; - border: white; - align-horizontal: center; -} diff --git a/sandbox/align.py b/sandbox/align.py deleted file mode 100644 index f3c3dc0f0..000000000 --- a/sandbox/align.py +++ /dev/null @@ -1,30 +0,0 @@ -from rich.style import Style - -from textual.app import App, ComposeResult -from textual.widget import Widget -from textual.widgets import Static - - -class Thing(Widget): - def render(self): - return "Hello, 3434 World.\n[b]Lorem impsum." - - -class AlignApp(App): - CSS_PATH = "align.css" - - def on_load(self): - self.bind("t", "log_tree") - - def compose(self) -> ComposeResult: - yield Thing(id="thing") - yield Static("foo", id="thing2") - yield Widget(id="thing3") - - def action_log_tree(self): - self.log(self.screen.tree) - - -if __name__ == "__main__": - app = AlignApp(css_path="align.css") - app.run() diff --git a/sandbox/auto_test.css b/sandbox/auto_test.css deleted file mode 100644 index 672517a24..000000000 --- a/sandbox/auto_test.css +++ /dev/null @@ -1,16 +0,0 @@ -Vertical { - background: red 50%; -} - -.test { - width: auto; - height: auto; - - background: white 50%; - border:solid green; - padding: 0; - margin:3; - - align: center middle; - box-sizing: border-box; -} diff --git a/sandbox/auto_test.py b/sandbox/auto_test.py deleted file mode 100644 index c3a071801..000000000 --- a/sandbox/auto_test.py +++ /dev/null @@ -1,21 +0,0 @@ -from textual.app import App, ComposeResult -from textual.widgets import Static -from textual.layout import Vertical - -from rich.text import Text - -TEXT = Text.from_markup(" ".join(str(n) * 5 for n in range(12))) - - -class AutoApp(App): - CSS_PATH = "auto_test.css" - - def compose(self) -> ComposeResult: - yield Vertical( - Static(TEXT, classes="test"), Static(TEXT, id="test", classes="test") - ) - - -if __name__ == "__main__": - app = AutoApp() - app.run() diff --git a/sandbox/borders.py b/sandbox/borders.py deleted file mode 100644 index 7dcb2dacd..000000000 --- a/sandbox/borders.py +++ /dev/null @@ -1,66 +0,0 @@ -from rich.console import RenderableType -from rich.text import Text - -from textual.app import App, ComposeResult -from textual.css.types import EdgeType -from textual.widget import Widget -from textual.widgets import Placeholder - - -class VerticalContainer(Widget): - DEFAULT_CSS = """ - VerticalContainer { - layout: vertical; - overflow: hidden auto; - background: darkblue; - } - - VerticalContainer Placeholder { - margin: 1 0; - height: auto; - align: center top; - } - """ - - -class Introduction(Widget): - DEFAULT_CSS = """ - Introduction { - background: indigo; - color: white; - height: 3; - padding: 1 0; - } - """ - - def render(self) -> RenderableType: - return Text("Here are the color edge types we support.", justify="center") - - -class BorderDemo(Widget): - def __init__(self, name: str): - super().__init__(name=name) - - def render(self) -> RenderableType: - return Text(self.name, style="black on yellow", justify="center") - - -class MyTestApp(App): - def compose(self) -> ComposeResult: - border_demo_widgets = [] - for border_edge_type in EdgeType.__args__: - border_demo = BorderDemo(f'"border: {border_edge_type} white"') - border_demo.styles.height = "auto" - border_demo.styles.margin = (1, 0) - border_demo.styles.border = (border_edge_type, "white") - border_demo_widgets.append(border_demo) - - yield VerticalContainer(Introduction(), *border_demo_widgets, id="root") - - def on_mount(self): - self.bind("q", "quit") - - -if __name__ == "__main__": - app = MyTestApp() - app.run() diff --git a/sandbox/buttons.css b/sandbox/buttons.css deleted file mode 100644 index 0d34ebf9e..000000000 --- a/sandbox/buttons.css +++ /dev/null @@ -1,6 +0,0 @@ - -Button { - box-sizing: border-box; - margin: 1; - width: 100%; -} diff --git a/sandbox/buttons.py b/sandbox/buttons.py deleted file mode 100644 index 883a457e2..000000000 --- a/sandbox/buttons.py +++ /dev/null @@ -1,33 +0,0 @@ -from textual import layout, events -from textual.app import App, ComposeResult -from textual.widgets import Button - - -class ButtonsApp(App[str]): - def compose(self) -> ComposeResult: - yield layout.Vertical( - Button("default", id="foo"), - Button.success("success", id="bar"), - Button.warning("warning", id="baz"), - Button.error("error", id="baz"), - ) - - def on_button_pressed(self, event: Button.Pressed) -> None: - self.app.bell() - - async def on_key(self, event: events.Key) -> None: - await self.dispatch_key(event) - - def key_d(self): - self.dark = not self.dark - - -app = ButtonsApp( - log_path="textual.log", - css_path="buttons.css", - watch_css=True, -) - -if __name__ == "__main__": - result = app.run() - print(repr(result)) diff --git a/sandbox/color_names.py b/sandbox/color_names.py deleted file mode 100644 index 464b65c0c..000000000 --- a/sandbox/color_names.py +++ /dev/null @@ -1,46 +0,0 @@ -import rich.repr -from rich.align import Align -from rich.console import RenderableType -from rich.panel import Panel -from rich.pretty import Pretty - -from textual._color_constants import COLOR_NAME_TO_RGB -from textual.app import App, ComposeResult -from textual.widget import Widget -from textual.widgets import Placeholder - - -@rich.repr.auto(angular=False) -class ColorDisplay(Widget, can_focus=True): - def render(self) -> RenderableType: - return Panel( - Align.center( - Pretty(self, no_wrap=True, overflow="ellipsis"), vertical="middle" - ), - title=self.name, - border_style="none", - ) - - -class ColorNames(App): - DEFAULT_CSS = """ - ColorDisplay { - height: 1; - } - """ - - def on_mount(self): - self.bind("q", "quit") - - def compose(self) -> ComposeResult: - for color_name, color in COLOR_NAME_TO_RGB.items(): - color_placeholder = ColorDisplay(name=color_name) - is_dark_color = sum(color) < 400 - color_placeholder.styles.color = "white" if is_dark_color else "black" - color_placeholder.styles.background = color_name - yield color_placeholder - - -if __name__ == "__main__": - color_name_app = ColorNames() - color_name_app.run() diff --git a/sandbox/dev_sandbox.py b/sandbox/dev_sandbox.py deleted file mode 100644 index 33c24d002..000000000 --- a/sandbox/dev_sandbox.py +++ /dev/null @@ -1,38 +0,0 @@ -from rich.console import RenderableType -from rich.panel import Panel -from rich.style import Style - -from textual.app import App -from textual.widget import Widget - - -class PanelWidget(Widget): - def render(self) -> RenderableType: - return Panel("hello world!", title="Title") - - -class BasicApp(App): - """Sandbox application used for testing/development by Textual developers""" - - def on_load(self): - """Bind keys here.""" - self.bind("tab", "toggle_class('#sidebar', '-active')") - self.bind("a", "toggle_class('#header', '-visible')") - self.bind("c", "toggle_class('#content', '-content-visible')") - self.bind("d", "toggle_class('#footer', 'dim')") - self.bind("x", "dump") - - def on_mount(self): - """Build layout here.""" - self.mount( - header=Widget(), - content=PanelWidget(), - footer=Widget(), - sidebar=Widget(), - ) - - def action_dump(self): - self.panic(self.tree) - - -BasicApp.run(css_path="dev_sandbox.scss", watch_css=True, log_path="textual.log") diff --git a/sandbox/dev_sandbox.scss b/sandbox/dev_sandbox.scss deleted file mode 100644 index d2232312a..000000000 --- a/sandbox/dev_sandbox.scss +++ /dev/null @@ -1,66 +0,0 @@ -/* CSS file for dev_sandbox.py */ - -$text: #f0f0f0; -$primary: #021720; -$secondary: #95d52a; -$background: #262626; - -$animatitext-speed: 500ms; -$animation: offset $animatitext-speed in_out_cubic; - -App > View { - docks: side=left/1; - background: $background; -} - -Widget:hover { - outline: heavy; - text-style: bold !important; -} - -#sidebar { - color: $text; - background: $background; - dock: side; - width: 30; - offset-x: -100%; - transition: $animation; - border-right: outer $secondary; -} - -#sidebar.-active { - offset-x: 0; -} - -#header { - color: $text; - background: $primary; - height: 3; - border-bottom: hkey $secondary; -} - -#header.-visible { - visibility: hidden; -} - -#content { - color: $text; - background: $background; - offset-y: -3; -} - -#content.-content-visible { - visibility: hidden; -} - -#footer { - text-opacity: 1; - color: $text; - background: $background; - height: 3; - border-top: hkey $secondary; -} - -#footer.dim { - text-opacity: 0.5; -} diff --git a/sandbox/fifty.py b/sandbox/fifty.py deleted file mode 100644 index c946e47ae..000000000 --- a/sandbox/fifty.py +++ /dev/null @@ -1,30 +0,0 @@ -from textual.app import App -from textual import layout -from textual.widget import Widget - - -class FiftyApp(App): - - DEFAULT_CSS = """ - Screen { - layout: vertical; - } - Horizontal { - height: 50%; - } - Widget { - width: 50%; - outline: white; - background: blue; - } - - """ - - def compose(self): - yield layout.Horizontal(Widget(), Widget()) - yield layout.Horizontal(Widget(), Widget()) - - -app = FiftyApp() -if __name__ == "__main__": - app.run() diff --git a/sandbox/horizontal.css b/sandbox/horizontal.css deleted file mode 100644 index 684820f00..000000000 --- a/sandbox/horizontal.css +++ /dev/null @@ -1,18 +0,0 @@ -Horizontal { - background: red 50%; - overflow-x: auto; - /* width: auto */ -} - -.test { - width: auto; - height: auto; - - background: white 50%; - border:solid green; - padding: 0; - margin:3; - - align: center middle; - box-sizing: content-box; -} diff --git a/sandbox/horizontal.py b/sandbox/horizontal.py deleted file mode 100644 index 041e03a8d..000000000 --- a/sandbox/horizontal.py +++ /dev/null @@ -1,26 +0,0 @@ -from textual.app import App, ComposeResult -from textual.widgets import Static -from textual import layout - -from rich.text import Text - -TEXT = Text.from_markup(" ".join(str(n) * 5 for n in range(10))) - - -class AutoApp(App): - def on_mount(self) -> None: - self.bind("t", "tree") - - def compose(self) -> ComposeResult: - yield layout.Horizontal( - Static(TEXT, classes="test"), Static(TEXT, id="test", classes="test") - ) - - def action_tree(self): - self.log(self.screen.tree) - - -app = AutoApp(css_path="horizontal.css") - -if __name__ == "__main__": - app.run() diff --git a/sandbox/input.py b/sandbox/input.py deleted file mode 100644 index 5b09e1364..000000000 --- a/sandbox/input.py +++ /dev/null @@ -1,67 +0,0 @@ -from __future__ import annotations - -from pathlib import Path - -from textual.app import App -from textual.widget import Widget - -from textual.widgets.text_input import TextInput, TextWidgetBase, TextArea - - -def celsius_to_fahrenheit(celsius: float) -> float: - return celsius * 1.8 + 32 - - -def fahrenheit_to_celsius(fahrenheit: float) -> float: - return (fahrenheit - 32) / 1.8 - - -words = set(Path("/usr/share/dict/words").read_text().splitlines()) - - -def word_autocompleter(value: str) -> str | None: - # An example autocompleter that uses the Unix dictionary to suggest - # word completions - for word in words: - if word.startswith(value): - return word - return None - - -class InputApp(App[str]): - def on_mount(self) -> None: - self.fahrenheit = TextInput(placeholder="Fahrenheit", id="fahrenheit") - self.celsius = TextInput(placeholder="Celsius", id="celsius") - self.fahrenheit.focus() - text_boxes = Widget(self.fahrenheit, self.celsius) - self.mount(inputs=text_boxes) - self.mount(spacer=Widget()) - self.mount( - top_search=Widget( - TextInput(autocompleter=word_autocompleter, id="topsearchbox") - ) - ) - self.mount( - footer=TextInput( - placeholder="Footer Search Bar", autocompleter=word_autocompleter - ) - ) - self.mount(text_area=TextArea()) - - def on_text_input_changed_changed(self, event: TextInput.Changed) -> None: - try: - value = float(event.value) - except ValueError: - return - if event.sender == self.celsius: - fahrenheit = celsius_to_fahrenheit(value) - self.fahrenheit.value = f"{fahrenheit:.1f}" - elif event.sender == self.fahrenheit: - celsius = fahrenheit_to_celsius(value) - self.celsius.value = f"{celsius:.1f}" - - -app = InputApp(log_path="textual.log", css_path="input.scss", watch_css=True) - -if __name__ == "__main__": - result = app.run() diff --git a/sandbox/input.scss b/sandbox/input.scss deleted file mode 100644 index edd89aea9..000000000 --- a/sandbox/input.scss +++ /dev/null @@ -1,53 +0,0 @@ -App { - background: $secondary; -} - -#spacer { - height: 1; - background: $primary-darken-2; - dock: top; -} - -Screen { - layout: dock; - docks: top=top bottom=bottom; - background: $background; -} - -#fahrenheit { - width: 50%; -} - -#celsius { - width: 50%; -} - -#celsius :focus { - border: heavy darkgoldenrod; -} - -#inputs { - dock: top; - background: $primary; - height: 3; - layout: horizontal; -} - -#text_area { - dock: bottom; -} - -#top_search { - dock: top; -} - -#topsearchbox { - width: 10%; -} - -#footer { - background: $primary-darken-2; - dock: bottom; - height: 1; - border: ; -} diff --git a/sandbox/local_styles.css b/sandbox/local_styles.css deleted file mode 100644 index 83cfefca4..000000000 --- a/sandbox/local_styles.css +++ /dev/null @@ -1,11 +0,0 @@ -App > View { - layout: dock; -} - -Widget { - text: on blue; -} - -Widget.-highlight { - outline: heavy red; -} diff --git a/sandbox/local_styles.py b/sandbox/local_styles.py deleted file mode 100644 index 63bfe9ac9..000000000 --- a/sandbox/local_styles.py +++ /dev/null @@ -1,36 +0,0 @@ -from textual import events -from textual.app import App -from textual.widget import Widget -from textual.widgets import Placeholder - - -class BasicApp(App): - """Sandbox application used for testing/development by Textual developers""" - - def on_mount(self): - """Build layout here.""" - self.mount( - header=Widget(), - content=Placeholder(), - footer=Widget(), - sidebar=Widget(), - ) - - async def on_key(self, event: events.Key) -> None: - await self.dispatch_key(event) - - def key_a(self) -> None: - footer = self.get_child("footer") - footer.set_styles(text="on magenta") - - def key_b(self) -> None: - footer = self.get_child("footer") - footer.set_styles("text: on green") - - def key_c(self) -> None: - header = self.get_child("header") - header.toggle_class("-highlight") - self.log(header.styles) - - -BasicApp.run(css_path="local_styles.css", log_path="textual.log") diff --git a/sandbox/nest.css b/sandbox/nest.css deleted file mode 100644 index 63abf3504..000000000 --- a/sandbox/nest.css +++ /dev/null @@ -1,25 +0,0 @@ - - -Vertical { - background: blue; - -} - -#container { - width:50%; - height: auto; - align-horizontal: center; - padding: 1; - border: heavy white; - background: white 50%; - overflow-y: auto -} - -TextWidget { - /* width: 50%; */ - height: auto; - padding: 2; - background: green 30%; - border: yellow; - box-sizing: border-box; -} \ No newline at end of file diff --git a/sandbox/nest.py b/sandbox/nest.py deleted file mode 100644 index ab643ff7a..000000000 --- a/sandbox/nest.py +++ /dev/null @@ -1,32 +0,0 @@ -from textual.app import App, ComposeResult -from textual.widget import Widget -from textual import layout - - -from rich.text import Text - - -lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." - -TEXT = Text.from_markup(lorem) - - -class TextWidget(Widget): - def render(self): - return TEXT - - -class AutoApp(App, css_path="nest.css"): - def on_mount(self) -> None: - self.bind("t", "tree") - - def compose(self) -> ComposeResult: - yield layout.Vertical( - Widget( - TextWidget(classes="test"), - id="container", - ), - ) - - def action_tree(self): - self.log(self.screen.tree) diff --git a/sandbox/scroll_to_widget.py b/sandbox/scroll_to_widget.py deleted file mode 100644 index 209439c16..000000000 --- a/sandbox/scroll_to_widget.py +++ /dev/null @@ -1,72 +0,0 @@ -from rich.console import RenderableType -from rich.text import Text - -from textual.app import App, ComposeResult -from textual.widget import Widget -from textual.widgets import Placeholder - -placeholders_count = 12 - - -class VerticalContainer(Widget): - DEFAULT_CSS = """ - VerticalContainer { - layout: vertical; - overflow: hidden auto; - background: darkblue; - } - - VerticalContainer Placeholder { - margin: 1 0; - height: 5; - border: solid lime; - align: center top; - } - """ - - -class Introduction(Widget): - DEFAULT_CSS = """ - Introduction { - background: indigo; - color: white; - height: 3; - padding: 1 0; - } - """ - - def render(self) -> RenderableType: - return Text( - "Press keys 0 to 9 to scroll to the Placeholder with that ID.", - justify="center", - ) - - -class MyTestApp(App): - def compose(self) -> ComposeResult: - placeholders = [ - Placeholder(id=f"placeholder_{i}", name=f"Placeholder #{i}") - for i in range(placeholders_count) - ] - - yield VerticalContainer(Introduction(), *placeholders, id="root") - - def on_mount(self): - self.bind("q", "quit") - self.bind("t", "tree") - for widget_index in range(placeholders_count): - self.bind(str(widget_index), f"scroll_to('placeholder_{widget_index}')") - - def action_tree(self): - self.log(self.tree) - - async def action_scroll_to(self, target_placeholder_id: str): - target_placeholder = self.query(f"#{target_placeholder_id}").first() - target_placeholder_container = self.query("#root").first() - target_placeholder_container.scroll_to_widget(target_placeholder, animate=True) - - -app = MyTestApp() - -if __name__ == "__main__": - app.run() diff --git a/sandbox/simplest.py b/sandbox/simplest.py deleted file mode 100644 index 1f1524485..000000000 --- a/sandbox/simplest.py +++ /dev/null @@ -1,3 +0,0 @@ -from textual.app import App - -app = App() diff --git a/sandbox/uber.css b/sandbox/uber.css deleted file mode 100644 index edfdebf80..000000000 --- a/sandbox/uber.css +++ /dev/null @@ -1,28 +0,0 @@ -App.-show-focus *:focus { - tint: #8bc34a 20%; -} - -#uber1 { - layout: vertical; - background: green; - overflow: hidden auto; - border: heavy white; - text-style: underline; - /* box-sizing: content-box; */ -} - -#uber1:focus-within { - background: darkslateblue; -} - -#child2 { - text-style: underline; - background: red 10%; -} - -.list-item { - height: 10; - /* display: none; */ - color: #12a0; - background: #ffffff00; -} diff --git a/sandbox/uber.py b/sandbox/uber.py deleted file mode 100644 index 72917dff2..000000000 --- a/sandbox/uber.py +++ /dev/null @@ -1,83 +0,0 @@ -import random -import sys - -from textual import events -from textual.app import App -from textual.widget import Widget -from textual.widgets import Placeholder - - -class BasicApp(App): - """Sandbox application used for testing/development by Textual developers""" - - def on_load(self): - self.bind("q", "quit", "Quit") - self.bind("d", "dump") - self.bind("t", "log_tree") - self.bind("p", "print") - self.bind("v", "toggle_visibility") - self.bind("x", "toggle_display") - self.bind("f", "modify_focussed") - self.bind("b", "toggle_border") - - async def on_mount(self): - """Build layout here.""" - first_child = Placeholder(id="child1", classes="list-item") - uber1 = Widget( - first_child, - Placeholder(id="child2", classes="list-item"), - Placeholder(id="child3", classes="list-item"), - Placeholder(classes="list-item"), - Placeholder(classes="list-item"), - Placeholder(classes="list-item"), - ) - self.mount(uber1=uber1) - uber1.focus() - self.first_child = first_child - self.uber = uber1 - - async def on_key(self, event: events.Key) -> None: - await self.dispatch_key(event) - - def action_quit(self): - self.panic(self.app.tree) - - def action_dump(self): - self.panic(str(self.app._registry)) - - def action_log_tree(self): - self.log(self.screen.tree) - - def action_print(self): - print( - "Focused widget is:", - self.focused, - ) - self.app.set_focus(None) - - def action_modify_focussed(self): - """Increment height of focussed child, randomise border and bg color""" - previous_height = self.focused.styles.height.value - new_height = previous_height + 1 - self.focused.styles.height = self.focused.styles.height.copy_with( - value=new_height - ) - color = random.choice(["red", "green", "blue"]) - self.focused.styles.background = color - self.focused.styles.border = ("dashed", color) - - def action_toggle_visibility(self): - self.focused.visible = not self.focused.visible - - def action_toggle_display(self): - # TODO: Doesn't work - self.focused.display = not self.focused.display - - def action_toggle_border(self): - self.focused.styles.border_top = ("solid", "invalid-color") - - -app = BasicApp(css_path="uber.css") - -if __name__ == "__main__": - app.run() diff --git a/sandbox/vertical.css b/sandbox/vertical.css deleted file mode 100644 index 6b3bd4287..000000000 --- a/sandbox/vertical.css +++ /dev/null @@ -1,22 +0,0 @@ -Screen { - background:blue; -} - -Vertical { - background: red 50%; - overflow: auto; - /* width: auto */ -} - -.test { - /* width: auto; */ - /* height: 50vh; */ - - background: white 50%; - border:solid green; - padding: 0; - margin:3; - - align: center middle; - box-sizing: border-box; -} \ No newline at end of file diff --git a/sandbox/vertical.py b/sandbox/vertical.py deleted file mode 100644 index 8bc64d929..000000000 --- a/sandbox/vertical.py +++ /dev/null @@ -1,29 +0,0 @@ -from textual.app import App, ComposeResult -from textual.widgets import Static -from textual import layout - -from rich.text import Text - -TEXT = Text.from_markup(" ".join(str(n) * 5 for n in range(10))) - - -class AutoApp(App): - def on_mount(self) -> None: - self.bind("t", "tree") - - def compose(self) -> ComposeResult: - yield layout.Horizontal( - layout.Vertical( - Static(TEXT, classes="test"), - Static(TEXT, id="test", classes="test"), - ) - ) - - def action_tree(self): - self.log(self.screen.tree) - - -app = AutoApp(css_path="vertical.css") - -if __name__ == "__main__": - app.run() diff --git a/sandbox/vertical_container.py b/sandbox/vertical_container.py deleted file mode 100644 index a06e1cd94..000000000 --- a/sandbox/vertical_container.py +++ /dev/null @@ -1,94 +0,0 @@ -from rich.console import RenderableType -from rich.text import Text - -from textual.app import App, ComposeResult -from textual.widget import Widget -from textual.widgets import Placeholder - -root_container_style = "border: solid white;" -initial_placeholders_count = 4 - - -class VerticalContainer(Widget): - DEFAULT_CSS = """ - VerticalContainer { - layout: vertical; - overflow: hidden auto; - background: darkblue; - ${root_container_style} - } - - VerticalContainer Placeholder { - margin: 1 0; - height: 5; - border: solid lime; - align: center top; - } - """.replace( - "${root_container_style}", root_container_style - ) - - -class Introduction(Widget): - DEFAULT_CSS = """ - Introduction { - background: indigo; - color: white; - height: 3; - padding: 1 0; - } - """ - - def render(self, styles) -> RenderableType: - return Text( - "Press '-' and '+' to add or remove placeholders.", justify="center" - ) - - -class MyTestApp(App): - def compose(self) -> ComposeResult: - # yield Introduction() - - placeholders = [ - Placeholder(id=f"placeholder_{i}", name=f"Placeholder #{i}") - for i in range(initial_placeholders_count) - ] - - yield VerticalContainer(Introduction(), *placeholders, id="root") - - def on_mount(self): - self.bind("q", "quit") - self.bind("t", "tree") - self.bind("-", "remove_placeholder") - self.bind("+", "add_placeholder") - - def action_tree(self): - self.log(self.tree) - - async def action_remove_placeholder(self): - placeholders = self.query("Placeholder") - placeholders_count = len(placeholders) - for i, placeholder in enumerate(placeholders): - if i == placeholders_count - 1: - await self.remove(placeholder) - placeholder.parent.children._nodes.remove(placeholder) - self.refresh(repaint=True, layout=True) - self.refresh_css() - - async def action_add_placeholder(self): - placeholders = self.query("Placeholder") - placeholders_count = len(placeholders) - placeholder = Placeholder( - id=f"placeholder_{placeholders_count}", - name=f"Placeholder #{placeholders_count}", - ) - root = self.get_child("root") - root.mount(placeholder) - self.refresh(repaint=True, layout=True) - self.refresh_css() - - -app = MyTestApp() - -if __name__ == "__main__": - app.run() From 0b23ccff1e80c2a55ab35e32a551aa4ff0116e5d Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 22 Sep 2022 14:56:34 +0100 Subject: [PATCH 43/46] Delete some old sandbox stuff --- sandbox/darren/focus_keybindings.py | 54 --------------- sandbox/darren/focus_keybindings.scss | 57 ---------------- src/textual/css/stylesheet.py | 96 --------------------------- 3 files changed, 207 deletions(-) delete mode 100644 sandbox/darren/focus_keybindings.py delete mode 100644 sandbox/darren/focus_keybindings.scss diff --git a/sandbox/darren/focus_keybindings.py b/sandbox/darren/focus_keybindings.py deleted file mode 100644 index fa5028747..000000000 --- a/sandbox/darren/focus_keybindings.py +++ /dev/null @@ -1,54 +0,0 @@ -from textual.app import App -from textual.widget import Widget -from textual.widgets import Static - - -class FocusKeybindsApp(App): - dark = True - - def on_load(self) -> None: - self.bind("1", "focus('widget1')") - self.bind("2", "focus('widget2')") - self.bind("3", "focus('widget3')") - self.bind("4", "focus('widget4')") - self.bind("q", "focus('widgetq')") - self.bind("w", "focus('widgetw')") - self.bind("e", "focus('widgete')") - self.bind("r", "focus('widgetr')") - - def on_mount(self) -> None: - info = Static( - "Use keybinds to shift focus between the widgets in the lists below", - ) - self.mount(info=info) - - self.mount( - body=Widget( - Widget( - Static("Press 1 to focus", id="widget1", classes="list-item"), - Static("Press 2 to focus", id="widget2", classes="list-item"), - Static("Press 3 to focus", id="widget3", classes="list-item"), - Static("Press 4 to focus", id="widget4", classes="list-item"), - classes="list", - id="left_list", - ), - Widget( - Static("Press Q to focus", id="widgetq", classes="list-item"), - Static("Press W to focus", id="widgetw", classes="list-item"), - Static("Press E to focus", id="widgete", classes="list-item"), - Static("Press R to focus", id="widgetr", classes="list-item"), - classes="list", - id="right_list", - ), - ), - ) - self.mount(footer=Static("No widget focused")) - - def on_descendant_focus(self): - self.get_child("footer").update( - f"Focused: {self.focused.id}" or "No widget focused" - ) - - -app = FocusKeybindsApp(css_path="focus_keybindings.scss", watch_css=True) -app.run() diff --git a/sandbox/darren/focus_keybindings.scss b/sandbox/darren/focus_keybindings.scss deleted file mode 100644 index a6803fe5a..000000000 --- a/sandbox/darren/focus_keybindings.scss +++ /dev/null @@ -1,57 +0,0 @@ -App > Screen { - layout: dock; - docks: left=left top=top; -} - -#info { - background: $primary; - dock: top; - height: 3; - padding: 1; -} - -#body { - dock: top; - layout: dock; - docks: bodylhs=left; -} - -#left_list { - dock: bodylhs; - padding: 2; -} - -#right_list { - dock: bodylhs; - padding: 2; -} - -#footer { - height: 1; - background: $secondary; - padding: 0 1; - dock: top; -} - -.list { - background: $surface; - border-top: hkey $surface-darken-1; -} - -.list:focus-within { - background: $primary-darken-1; - outline-top: $accent-lighten-1; - outline-bottom: $accent-lighten-1; -} - -.list-item { - background: $surface; - height: auto; - border: $surface-darken-1 tall; - padding: 0 1; -} - -.list-item:focus { - background: $surface-darken-1; - outline: $accent tall; -} diff --git a/src/textual/css/stylesheet.py b/src/textual/css/stylesheet.py index b5e964383..ef4ae6490 100644 --- a/src/textual/css/stylesheet.py +++ b/src/textual/css/stylesheet.py @@ -504,99 +504,3 @@ class Stylesheet: apply(node.horizontal_scrollbar) if node.show_horizontal_scrollbar and node.show_vertical_scrollbar: apply(node.scrollbar_corner) - - -if __name__ == "__main__": - from rich.traceback import install - - install(show_locals=True) - - class Widget(DOMNode): - pass - - class View(DOMNode): - pass - - class App(DOMNode): - pass - - app = App() - main_view = View(id="main") - help_view = View(id="help") - app._add_child(main_view) - app._add_child(help_view) - - widget1 = Widget(id="widget1") - widget2 = Widget(id="widget2") - sidebar = Widget(id="sidebar") - sidebar.add_class("float") - - helpbar = Widget(id="helpbar") - helpbar.add_class("float") - - main_view._add_child(widget1) - main_view._add_child(widget2) - main_view._add_child(sidebar) - - sub_view = View(id="sub") - sub_view.add_class("-subview") - main_view._add_child(sub_view) - - tooltip = Widget(id="tooltip") - tooltip.add_class("float", "transient") - sub_view._add_child(tooltip) - - help = Widget(id="markdown") - help_view._add_child(help) - help_view._add_child(helpbar) - - from rich import print - - print(app.tree) - print() - - DEFAULT_CSS = """ - App > View { - layout: dock; - docks: sidebar=left | widgets=top; - } - - #sidebar { - dock-group: sidebar; - } - - #widget1 { - text: on blue; - dock-group: widgets; - } - - #widget2 { - text: on red; - dock-group: widgets; - } - - """ - - stylesheet = Stylesheet() - stylesheet.add_source(CSS) - - print(stylesheet.css) - - # print(stylesheet.error_renderable) - - # print(widget1.styles) - - # stylesheet.apply(widget1) - - # print(widget1.styles) - - # print(stylesheet.css) - - # from .query import DOMQuery - - # tests = ["View", "App > View", "Widget.float", ".float.transient", "*"] - - # for test in tests: - # print("") - # print(f"[b]{test}") - # print(app.query(test)) From 3038652d75bb5a4e0b26db78ffe18ebb4872d89f Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 22 Sep 2022 14:57:32 +0100 Subject: [PATCH 44/46] Remove references to docks property --- src/textual/css/styles.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index 0c94e445c..e257475be 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -115,7 +115,6 @@ class RulesMap(TypedDict, total=False): max_height: Scalar dock: str - docks: tuple[DockGroup, ...] overflow_x: Overflow overflow_y: Overflow @@ -238,7 +237,6 @@ class StylesBase(ABC): max_height = ScalarProperty(percent_unit=Unit.HEIGHT, allow_auto=False) dock = DockProperty() - docks = DocksProperty() overflow_x = StringEnumProperty(VALID_OVERFLOW, "hidden") overflow_y = StringEnumProperty(VALID_OVERFLOW, "hidden") @@ -946,7 +944,6 @@ if __name__ == "__main__": styles.visibility = "hidden" styles.border = ("solid", "rgb(10,20,30)") styles.outline_right = ("solid", "red") - styles.docks = "foo bar" styles.text_style = "italic" styles.dock = "bar" styles.layers = "foo bar" From b3cf65c45ff81c85bb96687a87406422b7a2e15a Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 22 Sep 2022 14:59:32 +0100 Subject: [PATCH 45/46] Remove unused docks code --- src/textual/_arrange.py | 2 +- src/textual/css/_style_properties.py | 39 ---------------------------- src/textual/css/styles.py | 9 ------- 3 files changed, 1 insertion(+), 49 deletions(-) diff --git a/src/textual/_arrange.py b/src/textual/_arrange.py index c29b1c252..8c1032594 100644 --- a/src/textual/_arrange.py +++ b/src/textual/_arrange.py @@ -98,7 +98,7 @@ def arrange( ) dock_spacing = Spacing(top, right, bottom, left) - region = size.region.shrink(dock_spacing) + region = region.shrink(dock_spacing) layout_placements, arranged_layout_widgets = widget._layout.arrange( widget, layout_widgets, region.size ) diff --git a/src/textual/css/_style_properties.py b/src/textual/css/_style_properties.py index 4a45cfbbf..fc41a1a1b 100644 --- a/src/textual/css/_style_properties.py +++ b/src/textual/css/_style_properties.py @@ -511,45 +511,6 @@ class SpacingProperty: obj.refresh(layout=True) -class DocksProperty: - """Descriptor for getting and setting the docks property. This property - is used to define docks and their location on screen. - """ - - def __get__( - self, obj: StylesBase, objtype: type[StylesBase] | None = None - ) -> tuple[DockGroup, ...]: - """Get the Docks property - - Args: - obj (Styles): The ``Styles`` object. - objtype (type[Styles]): The ``Styles`` class. - - Returns: - tuple[DockGroup, ...]: A ``tuple`` containing the defined docks. - """ - if obj.has_rule("docks"): - return obj.get_rule("docks") - from .styles import DockGroup - - return (DockGroup("_default", "top", 1),) - - def __set__(self, obj: StylesBase, docks: Iterable[DockGroup] | None): - """Set the Docks property - - Args: - obj (Styles): The ``Styles`` object. - docks (Iterable[DockGroup]): Iterable of DockGroups - """ - _rich_traceback_omit = True - if docks is None: - if obj.clear_rule("docks"): - obj.refresh(layout=True) - else: - if obj.set_rule("docks", tuple(docks)): - obj.refresh(layout=True) - - class DockProperty: """Descriptor for getting and setting the dock property. The dock property allows you to specify which edge you want to fix a Widget to. diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index e257475be..4b2540cad 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -21,7 +21,6 @@ from ._style_properties import ( BoxProperty, ColorProperty, DockProperty, - DocksProperty, FractionalProperty, IntegerProperty, LayoutProperty, @@ -720,14 +719,6 @@ class Styles(StylesBase): append_declaration("offset", f"{x} {y}") if has_rule("dock"): append_declaration("dock", rules["dock"]) - if has_rule("docks"): - append_declaration( - "docks", - " ".join( - (f"{name}={edge}/{z}" if z else f"{name}={edge}") - for name, edge, z in rules["docks"] - ), - ) if has_rule("layers"): append_declaration("layers", " ".join(self.layers)) if has_rule("layer"): From 0ab5ba1b6fdc8680fb11f0d77ee970882ee47f4a Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 22 Sep 2022 15:27:49 +0100 Subject: [PATCH 46/46] Add missing __future__ import in dictionary example --- examples/dictionary.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/dictionary.py b/examples/dictionary.py index e128ae374..e0d756d11 100644 --- a/examples/dictionary.py +++ b/examples/dictionary.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import asyncio from typing import Any