Support multiple CSS files (#1079)

* Support multiple CSS paths

* Update a type to match docstring

* Ensure the demo app still works

* Use absolute paths in tests to (hopefully) appease Windows

* Notes about CSS changes in guide/docstrings, small grammar/typos fixes

* Move snapshot apps into snapshot_tests dir, improve messaging in snapshot output, add test for multiple css files interacting with classvar CSS

* Ensure consistent snapshot naming cross-platform

* Use rpartition instead of partition in import_app

* Fix handling of import_app when colon in arg

* Support paths containing Windows drive names in import_app

* Add note on new relative paths in snap_compare

* Update docs/guide/CSS.md

Co-authored-by: Will McGugan <willmcgugan@gmail.com>

* Fix formatting

* Update CHANGELOG to mention CSS_PATH supporting a list

Co-authored-by: Will McGugan <willmcgugan@gmail.com>
This commit is contained in:
darrenburns
2022-11-01 17:13:25 +00:00
committed by GitHub
parent 0403cfdc98
commit bbd811d671
19 changed files with 425 additions and 130 deletions

View File

@@ -344,7 +344,7 @@
'''
# ---
# name: test_css_property_snapshot[docs/examples/styles/align.py]
# name: test_css_property[align.py]
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
@@ -502,7 +502,7 @@
'''
# ---
# name: test_css_property_snapshot[docs/examples/styles/background.py]
# name: test_css_property[background.py]
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
@@ -657,7 +657,7 @@
'''
# ---
# name: test_css_property_snapshot[docs/examples/styles/border.py]
# name: test_css_property[border.py]
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
@@ -815,7 +815,7 @@
'''
# ---
# name: test_css_property_snapshot[docs/examples/styles/box_sizing.py]
# name: test_css_property[box_sizing.py]
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
@@ -971,7 +971,7 @@
'''
# ---
# name: test_css_property_snapshot[docs/examples/styles/color.py]
# name: test_css_property[color.py]
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
@@ -1128,7 +1128,7 @@
'''
# ---
# name: test_css_property_snapshot[docs/examples/styles/content_align.py]
# name: test_css_property[content_align.py]
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
@@ -1285,7 +1285,7 @@
'''
# ---
# name: test_css_property_snapshot[docs/examples/styles/display.py]
# name: test_css_property[display.py]
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
@@ -1441,7 +1441,7 @@
'''
# ---
# name: test_css_property_snapshot[docs/examples/styles/grid.py]
# name: test_css_property[grid.py]
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
@@ -1598,7 +1598,7 @@
'''
# ---
# name: test_css_property_snapshot[docs/examples/styles/height.py]
# name: test_css_property[height.py]
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
@@ -1754,7 +1754,7 @@
'''
# ---
# name: test_css_property_snapshot[docs/examples/styles/layout.py]
# name: test_css_property[layout.py]
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
@@ -1912,7 +1912,7 @@
'''
# ---
# name: test_css_property_snapshot[docs/examples/styles/links.py]
# name: test_css_property[links.py]
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
@@ -2069,7 +2069,7 @@
'''
# ---
# name: test_css_property_snapshot[docs/examples/styles/margin.py]
# name: test_css_property[margin.py]
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
@@ -2226,7 +2226,7 @@
'''
# ---
# name: test_css_property_snapshot[docs/examples/styles/offset.py]
# name: test_css_property[offset.py]
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
@@ -2384,7 +2384,7 @@
'''
# ---
# name: test_css_property_snapshot[docs/examples/styles/opacity.py]
# name: test_css_property[opacity.py]
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
@@ -2547,7 +2547,7 @@
'''
# ---
# name: test_css_property_snapshot[docs/examples/styles/outline.py]
# name: test_css_property[outline.py]
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
@@ -2704,7 +2704,7 @@
'''
# ---
# name: test_css_property_snapshot[docs/examples/styles/overflow.py]
# name: test_css_property[overflow.py]
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
@@ -2863,7 +2863,7 @@
'''
# ---
# name: test_css_property_snapshot[docs/examples/styles/padding.py]
# name: test_css_property[padding.py]
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
@@ -3018,7 +3018,7 @@
'''
# ---
# name: test_css_property_snapshot[docs/examples/styles/scrollbar_gutter.py]
# name: test_css_property[scrollbar_gutter.py]
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
@@ -3174,7 +3174,7 @@
'''
# ---
# name: test_css_property_snapshot[docs/examples/styles/scrollbar_size.py]
# name: test_css_property[scrollbar_size.py]
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
@@ -3330,7 +3330,7 @@
'''
# ---
# name: test_css_property_snapshot[docs/examples/styles/scrollbars.py]
# name: test_css_property[scrollbars.py]
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
@@ -3487,7 +3487,7 @@
'''
# ---
# name: test_css_property_snapshot[docs/examples/styles/text_align.py]
# name: test_css_property[text_align.py]
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
@@ -3649,7 +3649,7 @@
'''
# ---
# name: test_css_property_snapshot[docs/examples/styles/text_opacity.py]
# name: test_css_property[text_opacity.py]
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
@@ -3807,7 +3807,7 @@
'''
# ---
# name: test_css_property_snapshot[docs/examples/styles/text_style.py]
# name: test_css_property[text_style.py]
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
@@ -3965,7 +3965,7 @@
'''
# ---
# name: test_css_property_snapshot[docs/examples/styles/tint.py]
# name: test_css_property[tint.py]
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
@@ -4129,7 +4129,7 @@
'''
# ---
# name: test_css_property_snapshot[docs/examples/styles/visibility.py]
# name: test_css_property[visibility.py]
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
@@ -4285,7 +4285,7 @@
'''
# ---
# name: test_css_property_snapshot[docs/examples/styles/width.py]
# name: test_css_property[width.py]
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
@@ -6165,6 +6165,163 @@
'''
# ---
# name: test_multiple_css
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
<!-- Generated with Rich https://www.textualize.io -->
<style>
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Regular"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Fira Code";
src: local("FiraCode-Bold"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
font-style: bold;
font-weight: 700;
}
.terminal-1292433193-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}
.terminal-1292433193-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}
.terminal-1292433193-r1 { fill: #8b0000 }
.terminal-1292433193-r2 { fill: #c5c8c6 }
.terminal-1292433193-r3 { fill: #ff0000 }
.terminal-1292433193-r4 { fill: #e1e1e1 }
</style>
<defs>
<clipPath id="terminal-1292433193-clip-terminal">
<rect x="0" y="0" width="975.0" height="584.5999999999999" />
</clipPath>
<clipPath id="terminal-1292433193-line-0">
<rect x="0" y="1.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1292433193-line-1">
<rect x="0" y="25.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1292433193-line-2">
<rect x="0" y="50.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1292433193-line-3">
<rect x="0" y="74.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1292433193-line-4">
<rect x="0" y="99.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1292433193-line-5">
<rect x="0" y="123.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1292433193-line-6">
<rect x="0" y="147.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1292433193-line-7">
<rect x="0" y="172.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1292433193-line-8">
<rect x="0" y="196.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1292433193-line-9">
<rect x="0" y="221.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1292433193-line-10">
<rect x="0" y="245.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1292433193-line-11">
<rect x="0" y="269.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1292433193-line-12">
<rect x="0" y="294.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1292433193-line-13">
<rect x="0" y="318.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1292433193-line-14">
<rect x="0" y="343.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1292433193-line-15">
<rect x="0" y="367.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1292433193-line-16">
<rect x="0" y="391.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1292433193-line-17">
<rect x="0" y="416.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1292433193-line-18">
<rect x="0" y="440.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1292433193-line-19">
<rect x="0" y="465.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1292433193-line-20">
<rect x="0" y="489.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1292433193-line-21">
<rect x="0" y="513.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-1292433193-line-22">
<rect x="0" y="538.3" width="976" height="24.65"/>
</clipPath>
</defs>
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="633.6" rx="8"/><text class="terminal-1292433193-title" fill="#c5c8c6" text-anchor="middle" x="496" y="27">MultipleCSSApp</text>
<g transform="translate(26,22)">
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
<circle cx="44" cy="0" r="7" fill="#28c840"/>
</g>
<g transform="translate(9, 41)" clip-path="url(#terminal-1292433193-clip-terminal)">
<rect fill="#ff0000" x="0" y="1.5" width="48.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#ff0000" x="48.8" y="1.5" width="927.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#556b2f" x="0" y="25.9" width="48.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#556b2f" x="48.8" y="25.9" width="927.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="50.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="74.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="99.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="123.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="147.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="172.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="196.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="221.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="245.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="269.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="294.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="318.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="343.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="367.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="391.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="416.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="440.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="465.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="489.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="513.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="538.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="562.7" width="976" height="24.65" shape-rendering="crispEdges"/>
<g class="terminal-1292433193-matrix">
<text class="terminal-1292433193-r1" x="0" y="20" textLength="48.8" clip-path="url(#terminal-1292433193-line-0)">#one</text><text class="terminal-1292433193-r2" x="976" y="20" textLength="12.2" clip-path="url(#terminal-1292433193-line-0)">
</text><text class="terminal-1292433193-r3" x="0" y="44.4" textLength="48.8" clip-path="url(#terminal-1292433193-line-1)">#two</text><text class="terminal-1292433193-r2" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-1292433193-line-1)">
</text><text class="terminal-1292433193-r2" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-1292433193-line-2)">
</text><text class="terminal-1292433193-r2" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-1292433193-line-3)">
</text><text class="terminal-1292433193-r2" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-1292433193-line-4)">
</text><text class="terminal-1292433193-r2" x="976" y="142" textLength="12.2" clip-path="url(#terminal-1292433193-line-5)">
</text><text class="terminal-1292433193-r2" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-1292433193-line-6)">
</text><text class="terminal-1292433193-r2" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-1292433193-line-7)">
</text><text class="terminal-1292433193-r2" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-1292433193-line-8)">
</text><text class="terminal-1292433193-r2" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-1292433193-line-9)">
</text><text class="terminal-1292433193-r2" x="976" y="264" textLength="12.2" clip-path="url(#terminal-1292433193-line-10)">
</text><text class="terminal-1292433193-r2" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-1292433193-line-11)">
</text><text class="terminal-1292433193-r2" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-1292433193-line-12)">
</text><text class="terminal-1292433193-r2" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-1292433193-line-13)">
</text><text class="terminal-1292433193-r2" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-1292433193-line-14)">
</text><text class="terminal-1292433193-r2" x="976" y="386" textLength="12.2" clip-path="url(#terminal-1292433193-line-15)">
</text><text class="terminal-1292433193-r2" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-1292433193-line-16)">
</text><text class="terminal-1292433193-r2" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-1292433193-line-17)">
</text><text class="terminal-1292433193-r2" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-1292433193-line-18)">
</text><text class="terminal-1292433193-r2" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-1292433193-line-19)">
</text><text class="terminal-1292433193-r2" x="976" y="508" textLength="12.2" clip-path="url(#terminal-1292433193-line-20)">
</text><text class="terminal-1292433193-r2" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-1292433193-line-21)">
</text><text class="terminal-1292433193-r2" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-1292433193-line-22)">
</text>
</g>
</g>
</svg>
'''
# ---
# name: test_textlog_max_lines
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">

View File

@@ -6,7 +6,7 @@ from dataclasses import dataclass
from datetime import datetime
from operator import attrgetter
from os import PathLike
from pathlib import Path
from pathlib import Path, PurePath
from typing import Union, List, Optional, Callable, Iterable
import pytest
@@ -31,7 +31,7 @@ TEXTUAL_APP_KEY = pytest.StashKey[App]()
@pytest.fixture
def snap_compare(
snapshot: SnapshotAssertion, request: FixtureRequest
) -> Callable[[str], bool]:
) -> Callable[[str | PurePath], 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
@@ -39,7 +39,7 @@ def snap_compare(
"""
def compare(
app_path: str,
app_path: str | PurePath,
press: Iterable[str] = ("_",),
terminal_size: tuple[int, int] = (80, 24),
) -> bool:
@@ -50,7 +50,8 @@ def snap_compare(
the snapshot on disk will be updated to match the current screenshot.
Args:
app_path (str): The path of the app.
app_path (str): The path of the app. Relative paths are relative to the location of the
test this function is called from.
press (Iterable[str]): Key presses to run before taking screenshot. "_" is a short pause.
terminal_size (tuple[int, int]): A pair of integers (WIDTH, HEIGHT), representing terminal size.
@@ -58,7 +59,17 @@ def snap_compare(
bool: True if the screenshot matches the snapshot.
"""
node = request.node
app = import_app(app_path)
path = Path(app_path)
if path.is_absolute():
# If the user supplies an absolute path, just use it directly.
app = import_app(str(path.resolve()))
else:
# If a relative path is supplied by the user, it's relative to the location of the pytest node,
# NOT the location that `pytest` was invoked from.
node_path = node.path.parent
resolved = (node_path / app_path).resolve()
app = import_app(str(resolved))
actual_screenshot = take_svg_screenshot(
app=app,
press=press,
@@ -114,16 +125,19 @@ def pytest_sessionfinish(
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:
if app:
path, line_index, name = item.reportinfo()
similarity = (
100
* difflib.SequenceMatcher(
a=str(snapshot_svg), b=str(actual_svg)
).ratio()
)
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(),
file_similarity=similarity,
test_name=name,
path=path,
line_number=line_index + 1,

View File

@@ -0,0 +1,8 @@
#one {
background: green;
color: cyan;
}
#two {
color: red;
}

View File

@@ -0,0 +1,34 @@
"""Testing multiple CSS files, including app-level CSS
-- element #one
The `background` rule on #one tests a 3-way specificity clash between
classvar CSS and two separate CSS files. The background ends up red
because classvar CSS wins.
The `color` rule tests a clash between loading two external CSS files.
The color ends up as darkred (from 'second.css'), because that file is loaded
second and wins.
-- element #two
This element tests that separate rules applied to the same widget are mixed
correctly. The color is set to cadetblue in 'first.css', and the background is
darkolivegreen in 'second.css'. Both of these should apply.
"""
from textual.app import App, ComposeResult
from textual.widgets import Static
class MultipleCSSApp(App):
CSS = """
#one {
background: red;
}
"""
def compose(self) -> ComposeResult:
yield Static("#one", id="one")
yield Static("#two", id="two")
app = MultipleCSSApp(css_path=["first.css", "second.css"])
if __name__ == '__main__':
app.run()

View File

@@ -0,0 +1,8 @@
#one {
background: blue;
color: darkred;
}
#two {
background: darkolivegreen;
}

View File

@@ -61,6 +61,7 @@
{{ diff.path }}:{{ diff.line_number }}
</span>
</span>
{% if diff.snapshot != "" %}
<div class="form-check form-switch mt-1">
<input class="form-check-input" type="checkbox" role="switch"
id="flexSwitchCheckDefault" onchange="toggleOverlayCheckbox(this, {{ loop.index0 }})">
@@ -68,6 +69,7 @@
Show difference
</label>
</div>
{% endif %}
</div>
<div class="card-body">
<div class="row">
@@ -86,17 +88,38 @@
</div>
</div>
<div class="diff-wrapper-snapshot">
{{ diff.snapshot }}
{% if diff.snapshot != "" %}
{{ diff.snapshot }}
{% else %}
<div class="card">
<div class="card-body">
<h4>No history for this test</h4>
<p class="lead">If you're happy with the content on the left,
save it to disk by running pytest with the <code>--snapshot-update</code> flag.</p>
<h5>Unexpected?</h5>
<p class="lead">
Snapshots are named after the name of the test you call <code>snap_compare</code> in by default.
<br>
If you've renamed a test, the association between the snapshot and the test is lost,
and you'll need to run with <code>--snapshot-update</code> to associate the snapshot
with the new test name.
</p>
</div>
</div>
{% endif %}
</div>
{% if diff.snapshot != "" %}
<div class="w-100 d-flex justify-content-center mt-1">
<span class="small">Historical snapshot</span>
<span class="small">
Historical snapshot
</span>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{# Modal with debug info: #}
<div class="modal modal-lg fade" id="environmentModal" tabindex="-1"
aria-labelledby="environmentModalLabel"

View File

@@ -1,40 +1,40 @@
from pathlib import Path, PurePosixPath
from pathlib import Path
import pytest
from textual.app import App
from textual.widgets import Input, Button
WIDGET_EXAMPLES_DIR = Path("../../docs/examples/widgets")
LAYOUT_EXAMPLES_DIR = Path("../../docs/examples/guide/layout")
STYLES_EXAMPLES_DIR = Path("../../docs/examples/styles")
# --- Layout related stuff ---
def test_grid_layout_basic(snap_compare):
assert snap_compare("docs/examples/guide/layout/grid_layout1.py")
assert snap_compare(LAYOUT_EXAMPLES_DIR / "grid_layout1.py")
def test_grid_layout_basic_overflow(snap_compare):
assert snap_compare("docs/examples/guide/layout/grid_layout2.py")
assert snap_compare(LAYOUT_EXAMPLES_DIR / "grid_layout2.py")
def test_grid_layout_gutter(snap_compare):
assert snap_compare("docs/examples/guide/layout/grid_layout7_gutter.py")
assert snap_compare(LAYOUT_EXAMPLES_DIR / "grid_layout7_gutter.py")
def test_layers(snap_compare):
assert snap_compare("docs/examples/guide/layout/layers.py")
assert snap_compare(LAYOUT_EXAMPLES_DIR / "layers.py")
def test_horizontal_layout(snap_compare):
assert snap_compare("docs/examples/guide/layout/horizontal_layout.py")
assert snap_compare(LAYOUT_EXAMPLES_DIR / "horizontal_layout.py")
def test_vertical_layout(snap_compare):
assert snap_compare("docs/examples/guide/layout/vertical_layout.py")
assert snap_compare(LAYOUT_EXAMPLES_DIR / "vertical_layout.py")
def test_dock_layout_sidebar(snap_compare):
assert snap_compare("docs/examples/guide/layout/dock_layout2_sidebar.py")
assert snap_compare(LAYOUT_EXAMPLES_DIR / "dock_layout2_sidebar.py")
# --- Widgets - rendering and basic interactions ---
@@ -42,7 +42,6 @@ def test_dock_layout_sidebar(snap_compare):
# When adding a new widget, ideally we should also create a snapshot test
# from these examples which test rendering and simple interactions with it.
def test_checkboxes(snap_compare):
"""Tests checkboxes but also acts a regression test for using
width: auto in a Horizontal layout context."""
@@ -54,7 +53,7 @@ def test_checkboxes(snap_compare):
"enter", # toggle on
"wait:20",
]
assert snap_compare("docs/examples/widgets/checkbox.py", press=press)
assert snap_compare(WIDGET_EXAMPLES_DIR / "checkbox.py", press=press)
def test_input_and_focus(snap_compare):
@@ -64,33 +63,33 @@ def test_input_and_focus(snap_compare):
"tab",
*"Burns", # Tab focus to second input, write "Burns"
]
assert snap_compare("docs/examples/widgets/input.py", press=press)
assert snap_compare(WIDGET_EXAMPLES_DIR / "input.py", press=press)
def test_buttons_render(snap_compare):
# Testing button rendering. We press tab to focus the first button too.
assert snap_compare("docs/examples/widgets/button.py", press=["tab"])
assert snap_compare(WIDGET_EXAMPLES_DIR / "button.py", press=["tab"])
def test_datatable_render(snap_compare):
press = ["tab", "down", "down", "right", "up", "left"]
assert snap_compare("docs/examples/widgets/data_table.py", press=press)
assert snap_compare(WIDGET_EXAMPLES_DIR / "data_table.py", press=press)
def test_footer_render(snap_compare):
assert snap_compare("docs/examples/widgets/footer.py")
assert snap_compare(WIDGET_EXAMPLES_DIR / "footer.py")
def test_header_render(snap_compare):
assert snap_compare("docs/examples/widgets/header.py")
assert snap_compare(WIDGET_EXAMPLES_DIR / "header.py")
def test_textlog_max_lines(snap_compare):
assert snap_compare("tests/snapshots/textlog_max_lines.py", press=[*"abcde", "_"])
assert snap_compare("snapshot_apps/textlog_max_lines.py", press=[*"abcde", "_"])
def test_fr_units(snap_compare):
assert snap_compare("tests/snapshots/fr_units.py")
assert snap_compare("snapshot_apps/fr_units.py")
# --- CSS properties ---
@@ -98,12 +97,18 @@ def test_fr_units(snap_compare):
# If any of these change, something has likely broken, so snapshot each of them.
PATHS = [
str(PurePosixPath(path))
for path in Path("docs/examples/styles").iterdir()
path.name
for path in (Path(__file__).parent / STYLES_EXAMPLES_DIR).iterdir()
if path.suffix == ".py"
]
@pytest.mark.parametrize("path", PATHS)
def test_css_property_snapshot(path, snap_compare):
assert snap_compare(path)
@pytest.mark.parametrize("file_name", PATHS)
def test_css_property(file_name, snap_compare):
path_to_app = STYLES_EXAMPLES_DIR / file_name
assert snap_compare(path_to_app)
def test_multiple_css(snap_compare):
# Interaction between multiple CSS files and app-level/classvar CSS
assert snap_compare("snapshot_apps/multiple_css/multiple_css.py")

View File

@@ -1,43 +1,38 @@
from typing import Type
from __future__ import annotations
from pathlib import Path
import pytest
from textual.app import App
class RelativePathObjectApp(App[None]):
APP_DIR = Path(__file__).parent
class RelativePathObjectApp(App[None]):
CSS_PATH = Path("test.css")
class RelativePathStrApp(App[None]):
class RelativePathStrApp(App[None]):
CSS_PATH = "test.css"
class AbsolutePathObjectApp(App[None]):
class AbsolutePathObjectApp(App[None]):
CSS_PATH = Path("/tmp/test.css")
class AbsolutePathStrApp(App[None]):
class AbsolutePathStrApp(App[None]):
CSS_PATH = "/tmp/test.css"
def path_tester(obj_type: Type[App[None]], str_type: Type[App[None]], intended_result: Path) -> None:
assert isinstance(obj_type().css_path,Path), (
"CSS_PATH didn't stay as an object"
)
assert isinstance(str_type().css_path,Path), (
"CSS_PATH wasn't converted from str to Path"
)
assert obj_type().css_path == intended_result, (
"CSS_PATH doesn't match the intended result."
)
assert str_type().css_path == intended_result, (
"CSS_PATH doesn't match the intended result."
)
assert str_type().css_path == obj_type().css_path, (
"CSS_PATH str to Path conversion gave a different result"
)
def test_relative_path():
path_tester(RelativePathObjectApp, RelativePathStrApp, ((Path(__file__).absolute().parent ) / "test.css").absolute())
class ListPathApp(App[None]):
CSS_PATH = ["test.css", Path("/another/path.css")]
def test_absolute_path():
path_tester(AbsolutePathObjectApp, AbsolutePathStrApp, Path("/tmp/test.css").absolute())
@pytest.mark.parametrize("app,expected_css_path_attribute", [
(RelativePathObjectApp(), [APP_DIR / "test.css"]),
(RelativePathStrApp(), [APP_DIR / "test.css"]),
(AbsolutePathObjectApp(), [Path("/tmp/test.css")]),
(AbsolutePathStrApp(), [Path("/tmp/test.css")]),
(ListPathApp(), [APP_DIR / "test.css", Path("/another/path.css")]),
])
def test_css_paths_of_various_types(app, expected_css_path_attribute):
assert app.css_path == [path.absolute() for path in expected_css_path_attribute]