mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
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:
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Added `init` param to reactive.watch
|
- Added `init` param to reactive.watch
|
||||||
|
- `CSS_PATH` can now be a list of CSS files https://github.com/Textualize/textual/pull/1079
|
||||||
|
|
||||||
## [0.3.0] - 2022-10-31
|
## [0.3.0] - 2022-10-31
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Textual uses CSS to apply style to widgets. If you have any exposure to web deve
|
|||||||
|
|
||||||
## Stylesheets
|
## Stylesheets
|
||||||
|
|
||||||
CSS stands for _Cascading Stylesheets_. A stylesheet is a list of styles and rules about how those styles should be applied to a web page. In the case of Textual, the stylesheet applies [styles](./styles.md) to widgets but otherwise it is the same idea.
|
CSS stands for _Cascading Stylesheets_. A stylesheet is a list of styles and rules about how those styles should be applied to a web page. In the case of Textual, the stylesheet applies [styles](./styles.md) to widgets, but otherwise it is the same idea.
|
||||||
|
|
||||||
When Textual loads CSS it sets attributes on your widgets' `style` object. The effect is the same as if you had set attributes in Python.
|
When Textual loads CSS it sets attributes on your widgets' `style` object. The effect is the same as if you had set attributes in Python.
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ Header {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The lines inside the curly braces contains CSS _rules_, which consist of a rule name and rule value separated by a colon and ending in a semi-colon. Such rules are typically written one per line, but you could add additional rules as long as they are separated by semi-colons.
|
The lines inside the curly braces contains CSS _rules_, which consist of a rule name and rule value separated by a colon and ending in a semicolon. Such rules are typically written one per line, but you could add additional rules as long as they are separated by semicolons.
|
||||||
|
|
||||||
The first rule in the above example reads `"dock: top;"`. The rule name is `dock` which tells Textual to place the widget on an edge of the screen. The text after the colon is `top` which tells Textual to dock to the _top_ of the screen. Other valid values for `dock` are "right", "bottom", or "left"; but "top" is most appropriate for a header.
|
The first rule in the above example reads `"dock: top;"`. The rule name is `dock` which tells Textual to place the widget on an edge of the screen. The text after the colon is `top` which tells Textual to dock to the _top_ of the screen. Other valid values for `dock` are "right", "bottom", or "left"; but "top" is most appropriate for a header.
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ This doesn't look much like a tree yet. Let's add a header and a footer to this
|
|||||||
```{.textual path="docs/examples/guide/dom2.py"}
|
```{.textual path="docs/examples/guide/dom2.py"}
|
||||||
```
|
```
|
||||||
|
|
||||||
With a header and a footer widget the DOM looks the this:
|
With a header and a footer widget the DOM looks like this:
|
||||||
|
|
||||||
<div class="excalidraw">
|
<div class="excalidraw">
|
||||||
--8<-- "docs/images/dom2.excalidraw.svg"
|
--8<-- "docs/images/dom2.excalidraw.svg"
|
||||||
@@ -132,7 +132,7 @@ Here's the output from this example:
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
You may recognize some of the elements in the above screenshot, but it doesn't quite look like a dialog. This is because we haven't added a stylesheet.
|
You may recognize some elements in the above screenshot, but it doesn't quite look like a dialog. This is because we haven't added a stylesheet.
|
||||||
|
|
||||||
## CSS files
|
## CSS files
|
||||||
|
|
||||||
@@ -142,7 +142,8 @@ To add a stylesheet set the `CSS_PATH` classvar to a relative path:
|
|||||||
--8<-- "docs/examples/guide/dom4.py"
|
--8<-- "docs/examples/guide/dom4.py"
|
||||||
```
|
```
|
||||||
|
|
||||||
You may have noticed that some of the constructors have additional keyword arguments: `id` and `classes`. These are used by the CSS to identify parts of the DOM. We will cover these in the next section.
|
You may have noticed that some constructors have additional keyword arguments: `id` and `classes`.
|
||||||
|
These are used by the CSS to identify parts of the DOM. We will cover these in the next section.
|
||||||
|
|
||||||
Here's the CSS file we are applying:
|
Here's the CSS file we are applying:
|
||||||
|
|
||||||
@@ -158,6 +159,10 @@ With the CSS in place, the output looks very different:
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Using multiple CSS files
|
||||||
|
|
||||||
|
You can also set the `CSS_PATH` class variable to a list of paths. Textual will combine the rules from all of the supplied paths.
|
||||||
|
|
||||||
### Why CSS?
|
### Why CSS?
|
||||||
|
|
||||||
It is reasonable to ask why use CSS at all? Python is a powerful and expressive language. Wouldn't it be easier to set styles in your `.py` files?
|
It is reasonable to ask why use CSS at all? Python is a powerful and expressive language. Wouldn't it be easier to set styles in your `.py` files?
|
||||||
@@ -178,7 +183,7 @@ Being able to iterate on the design without restarting the application makes it
|
|||||||
|
|
||||||
A selector is the text which precedes the curly braces in a set of rules. It tells Textual which widgets it should apply the rules to.
|
A selector is the text which precedes the curly braces in a set of rules. It tells Textual which widgets it should apply the rules to.
|
||||||
|
|
||||||
Selectors can target a kind of widget or a very specific widget. For instance you could have a selector that modifies all buttons, or you could target an individual button used in one dialog. This gives you a lot of flexibility in customizing your user interface.
|
Selectors can target a kind of widget or a very specific widget. For instance, you could have a selector that modifies all buttons, or you could target an individual button used in one dialog. This gives you a lot of flexibility in customizing your user interface.
|
||||||
|
|
||||||
Let's look at the selectors supported by Textual CSS.
|
Let's look at the selectors supported by Textual CSS.
|
||||||
|
|
||||||
@@ -201,7 +206,7 @@ Button {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The type selector will also match a widget's base classes. Consequently a `Static` selector will also style the button because the `Button` Python class extends `Static`.
|
The type selector will also match a widget's base classes. Consequently, a `Static` selector will also style the button because the `Button` Python class extends `Static`.
|
||||||
|
|
||||||
```sass
|
```sass
|
||||||
Static {
|
Static {
|
||||||
|
|||||||
@@ -35,8 +35,13 @@ def import_app(import_name: str) -> App:
|
|||||||
from textual.app import App, WINDOWS
|
from textual.app import App, WINDOWS
|
||||||
|
|
||||||
import_name, *argv = shlex.split(import_name, posix=not WINDOWS)
|
import_name, *argv = shlex.split(import_name, posix=not WINDOWS)
|
||||||
|
drive, import_name = os.path.splitdrive(import_name)
|
||||||
|
|
||||||
lib, _colon, name = import_name.partition(":")
|
lib, _colon, name = import_name.partition(":")
|
||||||
|
|
||||||
|
if drive:
|
||||||
|
lib = os.path.join(drive, os.sep, lib)
|
||||||
|
|
||||||
if lib.endswith(".py"):
|
if lib.endswith(".py"):
|
||||||
path = os.path.abspath(lib)
|
path = os.path.abspath(lib)
|
||||||
sys.path.append(str(Path(path).parent))
|
sys.path.append(str(Path(path).parent))
|
||||||
@@ -62,7 +67,7 @@ def import_app(import_name: str) -> App:
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
raise AppFail(f"App {name!r} not found in {lib!r}")
|
raise AppFail(f"App {name!r} not found in {lib!r}")
|
||||||
else:
|
else:
|
||||||
# Find a App class or instance that is *not* the base class
|
# Find an App class or instance that is *not* the base class
|
||||||
apps = [
|
apps = [
|
||||||
value
|
value
|
||||||
for value in global_vars.values()
|
for value in global_vars.values()
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import sys
|
|
||||||
from pathlib import Path, PurePath
|
from pathlib import Path, PurePath
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from typing import (
|
|||||||
TypeVar,
|
TypeVar,
|
||||||
cast,
|
cast,
|
||||||
Union,
|
Union,
|
||||||
|
List,
|
||||||
)
|
)
|
||||||
from weakref import WeakSet, WeakValueDictionary
|
from weakref import WeakSet, WeakValueDictionary
|
||||||
|
|
||||||
@@ -126,6 +127,10 @@ class ScreenStackError(ScreenError):
|
|||||||
"""Raised when attempting to pop the last screen from the stack."""
|
"""Raised when attempting to pop the last screen from the stack."""
|
||||||
|
|
||||||
|
|
||||||
|
class CssPathError(Exception):
|
||||||
|
"""Raised when supplied CSS path(s) are invalid."""
|
||||||
|
|
||||||
|
|
||||||
ReturnType = TypeVar("ReturnType")
|
ReturnType = TypeVar("ReturnType")
|
||||||
|
|
||||||
|
|
||||||
@@ -137,23 +142,27 @@ class _NullFile:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
CSSPathType = Union[str, PurePath, None]
|
CSSPathType = Union[str, PurePath, List[Union[str, PurePath]], None]
|
||||||
|
|
||||||
|
|
||||||
@rich.repr.auto
|
@rich.repr.auto
|
||||||
class App(Generic[ReturnType], DOMNode):
|
class App(Generic[ReturnType], DOMNode):
|
||||||
"""The base class for Textual Applications.
|
"""The base class for Textual Applications.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
driver_class (Type[Driver] | None, optional): Driver class or ``None`` to auto-detect. Defaults to None.
|
driver_class (Type[Driver] | None, optional): Driver class or ``None`` to auto-detect. Defaults to None.
|
||||||
css_path (str | PurePath | None, optional): Path to CSS or ``None`` for no CSS file. Defaults to None.
|
css_path (str | PurePath | list[str | PurePath] | None, optional): Path to CSS or ``None`` for no CSS file.
|
||||||
|
Defaults to None. To load multiple CSS files, pass a list of strings or paths which will be loaded in order.
|
||||||
watch_css (bool, optional): Watch CSS for changes. Defaults to False.
|
watch_css (bool, optional): Watch CSS for changes. Defaults to False.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
CssPathError: When the supplied CSS path(s) are an unexpected type.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Inline CSS for quick scripts (generally css_path should be preferred.)
|
|
||||||
CSS = ""
|
CSS = ""
|
||||||
|
"""Inline CSS, useful for quick scripts. This is loaded after CSS_PATH,
|
||||||
|
and therefore takes priority in the event of a specificity clash."""
|
||||||
|
|
||||||
# Default (lowest priority) CSS
|
# Default (the lowest priority) CSS
|
||||||
DEFAULT_CSS = """
|
DEFAULT_CSS = """
|
||||||
App {
|
App {
|
||||||
background: $background;
|
background: $background;
|
||||||
@@ -227,15 +236,30 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
self.stylesheet = Stylesheet(variables=self.get_css_variables())
|
self.stylesheet = Stylesheet(variables=self.get_css_variables())
|
||||||
self._require_stylesheet_update: set[DOMNode] = set()
|
self._require_stylesheet_update: set[DOMNode] = set()
|
||||||
|
|
||||||
# We want the CSS path to be resolved from the location of the App subclass
|
|
||||||
css_path = css_path or self.CSS_PATH
|
css_path = css_path or self.CSS_PATH
|
||||||
if css_path is not None:
|
if css_path is not None:
|
||||||
|
# When value(s) are supplied for CSS_PATH, we normalise them to a list of Paths.
|
||||||
if isinstance(css_path, str):
|
if isinstance(css_path, str):
|
||||||
css_path = Path(css_path)
|
css_paths = [Path(css_path)]
|
||||||
css_path = _make_path_object_relative(css_path, self) if css_path else None
|
elif isinstance(css_path, PurePath):
|
||||||
|
css_paths = [css_path]
|
||||||
|
elif isinstance(css_path, list):
|
||||||
|
css_paths = []
|
||||||
|
for path in css_path:
|
||||||
|
css_paths.append(Path(path) if isinstance(path, str) else path)
|
||||||
|
else:
|
||||||
|
raise CssPathError(
|
||||||
|
"Expected a str, Path or list[str | Path] for the CSS_PATH."
|
||||||
|
)
|
||||||
|
|
||||||
self.css_path = css_path
|
# We want the CSS path to be resolved from the location of the App subclass
|
||||||
|
css_paths = [
|
||||||
|
_make_path_object_relative(css_path, self) for css_path in css_paths
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
css_paths = []
|
||||||
|
|
||||||
|
self.css_path = css_paths
|
||||||
self._registry: WeakSet[DOMNode] = WeakSet()
|
self._registry: WeakSet[DOMNode] = WeakSet()
|
||||||
|
|
||||||
self._installed_screens: WeakValueDictionary[
|
self._installed_screens: WeakValueDictionary[
|
||||||
@@ -774,15 +798,16 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
|
|
||||||
async def _on_css_change(self) -> None:
|
async def _on_css_change(self) -> None:
|
||||||
"""Called when the CSS changes (if watch_css is True)."""
|
"""Called when the CSS changes (if watch_css is True)."""
|
||||||
if self.css_path is not None:
|
css_paths = self.css_path
|
||||||
|
if css_paths:
|
||||||
try:
|
try:
|
||||||
time = perf_counter()
|
time = perf_counter()
|
||||||
stylesheet = self.stylesheet.copy()
|
stylesheet = self.stylesheet.copy()
|
||||||
stylesheet.read(self.css_path)
|
stylesheet.read_all(css_paths)
|
||||||
stylesheet.parse()
|
stylesheet.parse()
|
||||||
elapsed = (perf_counter() - time) * 1000
|
elapsed = (perf_counter() - time) * 1000
|
||||||
self.log.system(
|
self.log.system(
|
||||||
f"<stylesheet> loaded {self.css_path!r} in {elapsed:.0f} ms"
|
f"<stylesheet> loaded {len(css_paths)} CSS files in {elapsed:.0f} ms"
|
||||||
)
|
)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
# TODO: Catch specific exceptions
|
# TODO: Catch specific exceptions
|
||||||
@@ -1141,8 +1166,8 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
self.log.system(features=self.features)
|
self.log.system(features=self.features)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if self.css_path is not None:
|
if self.css_path:
|
||||||
self.stylesheet.read(self.css_path)
|
self.stylesheet.read_all(self.css_path)
|
||||||
for path, css, tie_breaker in self.get_default_css():
|
for path, css, tie_breaker in self.get_default_css():
|
||||||
self.stylesheet.add_source(
|
self.stylesheet.add_source(
|
||||||
css, path=path, is_default_css=True, tie_breaker=tie_breaker
|
css, path=path, is_default_css=True, tie_breaker=tie_breaker
|
||||||
|
|||||||
@@ -248,11 +248,24 @@ class Stylesheet:
|
|||||||
with open(filename, "rt") as css_file:
|
with open(filename, "rt") as css_file:
|
||||||
css = css_file.read()
|
css = css_file.read()
|
||||||
path = os.path.abspath(filename)
|
path = os.path.abspath(filename)
|
||||||
except Exception as error:
|
except Exception:
|
||||||
raise StylesheetError(f"unable to read CSS file {filename!r}") from None
|
raise StylesheetError(f"unable to read CSS file {filename!r}") from None
|
||||||
self.source[str(path)] = CssSource(css, False, 0)
|
self.source[str(path)] = CssSource(css, False, 0)
|
||||||
self._require_parse = True
|
self._require_parse = True
|
||||||
|
|
||||||
|
def read_all(self, paths: list[PurePath]) -> None:
|
||||||
|
"""Read multiple CSS files, in order.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
paths (list[PurePath]): The paths of the CSS files to read, in order.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
StylesheetError: If the CSS could not be read.
|
||||||
|
StylesheetParseError: If the CSS is invalid.
|
||||||
|
"""
|
||||||
|
for path in paths:
|
||||||
|
self.read(path)
|
||||||
|
|
||||||
def add_source(
|
def add_source(
|
||||||
self,
|
self,
|
||||||
css: str,
|
css: str,
|
||||||
@@ -268,6 +281,7 @@ class Stylesheet:
|
|||||||
Defaults to None.
|
Defaults to None.
|
||||||
is_default_css (bool): True if the CSS is defined in the Widget, False if the CSS is defined
|
is_default_css (bool): True if the CSS is defined in the Widget, False if the CSS is defined
|
||||||
in a user stylesheet.
|
in a user stylesheet.
|
||||||
|
tie_breaker (int): Integer representing the priority of this source.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
StylesheetError: If the CSS could not be read.
|
StylesheetError: If the CSS could not be read.
|
||||||
|
|||||||
@@ -319,7 +319,7 @@ class DemoApp(App):
|
|||||||
self.query_one(TextLog).write(renderable)
|
self.query_one(TextLog).write(renderable)
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
example_css = "\n".join(Path(self.css_path).read_text().splitlines()[:50])
|
example_css = "\n".join(Path(self.css_path[0]).read_text().splitlines()[:50])
|
||||||
yield Container(
|
yield Container(
|
||||||
Sidebar(classes="-hidden"),
|
Sidebar(classes="-hidden"),
|
||||||
Header(show_clock=True),
|
Header(show_clock=True),
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from pathlib import PurePath
|
from pathlib import PurePath
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
@@ -9,20 +11,20 @@ from ._callback import invoke
|
|||||||
|
|
||||||
@rich.repr.auto
|
@rich.repr.auto
|
||||||
class FileMonitor:
|
class FileMonitor:
|
||||||
"""Monitors a file for changes and invokes a callback when it does."""
|
"""Monitors files for changes and invokes a callback when it does."""
|
||||||
|
|
||||||
def __init__(self, path: PurePath, callback: Callable) -> None:
|
def __init__(self, paths: list[PurePath], callback: Callable) -> None:
|
||||||
self.path = path
|
self.paths = paths
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
self._modified = self._get_modified()
|
self._modified = self._get_last_modified_time()
|
||||||
|
|
||||||
def _get_modified(self) -> float:
|
def _get_last_modified_time(self) -> float:
|
||||||
"""Get the modified time for a file being watched."""
|
"""Get the most recent modified time out of all files being watched."""
|
||||||
return os.stat(self.path).st_mtime
|
return max(os.stat(path).st_mtime for path in self.paths)
|
||||||
|
|
||||||
def check(self) -> bool:
|
def check(self) -> bool:
|
||||||
"""Check the monitored file. Return True if it was changed."""
|
"""Check the monitored files. Return True if any were changed since the last modification time."""
|
||||||
modified = self._get_modified()
|
modified = self._get_last_modified_time()
|
||||||
changed = modified != self._modified
|
changed = modified != self._modified
|
||||||
self._modified = modified
|
self._modified = modified
|
||||||
return changed
|
return changed
|
||||||
@@ -32,5 +34,5 @@ class FileMonitor:
|
|||||||
await self.on_change()
|
await self.on_change()
|
||||||
|
|
||||||
async def on_change(self) -> None:
|
async def on_change(self) -> None:
|
||||||
"""Called when file changes."""
|
"""Called when any of the monitored files change."""
|
||||||
await invoke(self.callback)
|
await invoke(self.callback)
|
||||||
|
|||||||
@@ -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">
|
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Generated with Rich https://www.textualize.io -->
|
<!-- 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">
|
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Generated with Rich https://www.textualize.io -->
|
<!-- 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">
|
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Generated with Rich https://www.textualize.io -->
|
<!-- 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">
|
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Generated with Rich https://www.textualize.io -->
|
<!-- 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">
|
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Generated with Rich https://www.textualize.io -->
|
<!-- 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">
|
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Generated with Rich https://www.textualize.io -->
|
<!-- 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">
|
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Generated with Rich https://www.textualize.io -->
|
<!-- 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">
|
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Generated with Rich https://www.textualize.io -->
|
<!-- 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">
|
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Generated with Rich https://www.textualize.io -->
|
<!-- 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">
|
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Generated with Rich https://www.textualize.io -->
|
<!-- 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">
|
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Generated with Rich https://www.textualize.io -->
|
<!-- 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">
|
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Generated with Rich https://www.textualize.io -->
|
<!-- 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">
|
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Generated with Rich https://www.textualize.io -->
|
<!-- 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">
|
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Generated with Rich https://www.textualize.io -->
|
<!-- 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">
|
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Generated with Rich https://www.textualize.io -->
|
<!-- 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">
|
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Generated with Rich https://www.textualize.io -->
|
<!-- 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">
|
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Generated with Rich https://www.textualize.io -->
|
<!-- 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">
|
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Generated with Rich https://www.textualize.io -->
|
<!-- 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">
|
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Generated with Rich https://www.textualize.io -->
|
<!-- 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">
|
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Generated with Rich https://www.textualize.io -->
|
<!-- 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">
|
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Generated with Rich https://www.textualize.io -->
|
<!-- 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">
|
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Generated with Rich https://www.textualize.io -->
|
<!-- 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">
|
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Generated with Rich https://www.textualize.io -->
|
<!-- 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">
|
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Generated with Rich https://www.textualize.io -->
|
<!-- 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">
|
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Generated with Rich https://www.textualize.io -->
|
<!-- 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">
|
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||||
<!-- Generated with Rich https://www.textualize.io -->
|
<!-- 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
|
# name: test_textlog_max_lines
|
||||||
'''
|
'''
|
||||||
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from dataclasses import dataclass
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
from os import PathLike
|
from os import PathLike
|
||||||
from pathlib import Path
|
from pathlib import Path, PurePath
|
||||||
from typing import Union, List, Optional, Callable, Iterable
|
from typing import Union, List, Optional, Callable, Iterable
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -31,7 +31,7 @@ TEXTUAL_APP_KEY = pytest.StashKey[App]()
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def snap_compare(
|
def snap_compare(
|
||||||
snapshot: SnapshotAssertion, request: FixtureRequest
|
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
|
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
|
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(
|
def compare(
|
||||||
app_path: str,
|
app_path: str | PurePath,
|
||||||
press: Iterable[str] = ("_",),
|
press: Iterable[str] = ("_",),
|
||||||
terminal_size: tuple[int, int] = (80, 24),
|
terminal_size: tuple[int, int] = (80, 24),
|
||||||
) -> bool:
|
) -> bool:
|
||||||
@@ -50,7 +50,8 @@ def snap_compare(
|
|||||||
the snapshot on disk will be updated to match the current screenshot.
|
the snapshot on disk will be updated to match the current screenshot.
|
||||||
|
|
||||||
Args:
|
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.
|
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.
|
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.
|
bool: True if the screenshot matches the snapshot.
|
||||||
"""
|
"""
|
||||||
node = request.node
|
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(
|
actual_screenshot = take_svg_screenshot(
|
||||||
app=app,
|
app=app,
|
||||||
press=press,
|
press=press,
|
||||||
@@ -114,16 +125,19 @@ def pytest_sessionfinish(
|
|||||||
actual_svg = item.stash.get(TEXTUAL_ACTUAL_SVG_KEY, None)
|
actual_svg = item.stash.get(TEXTUAL_ACTUAL_SVG_KEY, None)
|
||||||
app = item.stash.get(TEXTUAL_APP_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()
|
path, line_index, name = item.reportinfo()
|
||||||
|
similarity = (
|
||||||
|
100
|
||||||
|
* difflib.SequenceMatcher(
|
||||||
|
a=str(snapshot_svg), b=str(actual_svg)
|
||||||
|
).ratio()
|
||||||
|
)
|
||||||
diffs.append(
|
diffs.append(
|
||||||
SvgSnapshotDiff(
|
SvgSnapshotDiff(
|
||||||
snapshot=str(snapshot_svg),
|
snapshot=str(snapshot_svg),
|
||||||
actual=str(actual_svg),
|
actual=str(actual_svg),
|
||||||
file_similarity=100
|
file_similarity=similarity,
|
||||||
* difflib.SequenceMatcher(
|
|
||||||
a=str(snapshot_svg), b=str(actual_svg)
|
|
||||||
).ratio(),
|
|
||||||
test_name=name,
|
test_name=name,
|
||||||
path=path,
|
path=path,
|
||||||
line_number=line_index + 1,
|
line_number=line_index + 1,
|
||||||
|
|||||||
0
tests/snapshot_tests/snapshot_apps/__init__.py
Normal file
0
tests/snapshot_tests/snapshot_apps/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#one {
|
||||||
|
background: green;
|
||||||
|
color: cyan;
|
||||||
|
}
|
||||||
|
|
||||||
|
#two {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#one {
|
||||||
|
background: blue;
|
||||||
|
color: darkred;
|
||||||
|
}
|
||||||
|
|
||||||
|
#two {
|
||||||
|
background: darkolivegreen;
|
||||||
|
}
|
||||||
@@ -61,6 +61,7 @@
|
|||||||
{{ diff.path }}:{{ diff.line_number }}
|
{{ diff.path }}:{{ diff.line_number }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
{% if diff.snapshot != "" %}
|
||||||
<div class="form-check form-switch mt-1">
|
<div class="form-check form-switch mt-1">
|
||||||
<input class="form-check-input" type="checkbox" role="switch"
|
<input class="form-check-input" type="checkbox" role="switch"
|
||||||
id="flexSwitchCheckDefault" onchange="toggleOverlayCheckbox(this, {{ loop.index0 }})">
|
id="flexSwitchCheckDefault" onchange="toggleOverlayCheckbox(this, {{ loop.index0 }})">
|
||||||
@@ -68,6 +69,7 @@
|
|||||||
Show difference
|
Show difference
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -86,17 +88,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="diff-wrapper-snapshot">
|
<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>
|
</div>
|
||||||
|
{% if diff.snapshot != "" %}
|
||||||
<div class="w-100 d-flex justify-content-center mt-1">
|
<div class="w-100 d-flex justify-content-center mt-1">
|
||||||
<span class="small">Historical snapshot</span>
|
<span class="small">
|
||||||
|
Historical snapshot
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{# Modal with debug info: #}
|
{# Modal with debug info: #}
|
||||||
<div class="modal modal-lg fade" id="environmentModal" tabindex="-1"
|
<div class="modal modal-lg fade" id="environmentModal" tabindex="-1"
|
||||||
aria-labelledby="environmentModalLabel"
|
aria-labelledby="environmentModalLabel"
|
||||||
|
|||||||
@@ -1,40 +1,40 @@
|
|||||||
from pathlib import Path, PurePosixPath
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from textual.app import App
|
WIDGET_EXAMPLES_DIR = Path("../../docs/examples/widgets")
|
||||||
from textual.widgets import Input, Button
|
LAYOUT_EXAMPLES_DIR = Path("../../docs/examples/guide/layout")
|
||||||
|
STYLES_EXAMPLES_DIR = Path("../../docs/examples/styles")
|
||||||
|
|
||||||
|
|
||||||
# --- Layout related stuff ---
|
# --- Layout related stuff ---
|
||||||
|
|
||||||
|
|
||||||
def test_grid_layout_basic(snap_compare):
|
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):
|
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):
|
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):
|
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):
|
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):
|
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):
|
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 ---
|
# --- 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
|
# When adding a new widget, ideally we should also create a snapshot test
|
||||||
# from these examples which test rendering and simple interactions with it.
|
# from these examples which test rendering and simple interactions with it.
|
||||||
|
|
||||||
|
|
||||||
def test_checkboxes(snap_compare):
|
def test_checkboxes(snap_compare):
|
||||||
"""Tests checkboxes but also acts a regression test for using
|
"""Tests checkboxes but also acts a regression test for using
|
||||||
width: auto in a Horizontal layout context."""
|
width: auto in a Horizontal layout context."""
|
||||||
@@ -54,7 +53,7 @@ def test_checkboxes(snap_compare):
|
|||||||
"enter", # toggle on
|
"enter", # toggle on
|
||||||
"wait:20",
|
"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):
|
def test_input_and_focus(snap_compare):
|
||||||
@@ -64,33 +63,33 @@ def test_input_and_focus(snap_compare):
|
|||||||
"tab",
|
"tab",
|
||||||
*"Burns", # Tab focus to second input, write "Burns"
|
*"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):
|
def test_buttons_render(snap_compare):
|
||||||
# Testing button rendering. We press tab to focus the first button too.
|
# 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):
|
def test_datatable_render(snap_compare):
|
||||||
press = ["tab", "down", "down", "right", "up", "left"]
|
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):
|
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):
|
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):
|
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):
|
def test_fr_units(snap_compare):
|
||||||
assert snap_compare("tests/snapshots/fr_units.py")
|
assert snap_compare("snapshot_apps/fr_units.py")
|
||||||
|
|
||||||
|
|
||||||
# --- CSS properties ---
|
# --- 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.
|
# If any of these change, something has likely broken, so snapshot each of them.
|
||||||
|
|
||||||
PATHS = [
|
PATHS = [
|
||||||
str(PurePosixPath(path))
|
path.name
|
||||||
for path in Path("docs/examples/styles").iterdir()
|
for path in (Path(__file__).parent / STYLES_EXAMPLES_DIR).iterdir()
|
||||||
if path.suffix == ".py"
|
if path.suffix == ".py"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("path", PATHS)
|
@pytest.mark.parametrize("file_name", PATHS)
|
||||||
def test_css_property_snapshot(path, snap_compare):
|
def test_css_property(file_name, snap_compare):
|
||||||
assert snap_compare(path)
|
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")
|
||||||
|
|||||||
@@ -1,43 +1,38 @@
|
|||||||
from typing import Type
|
from __future__ import annotations
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import pytest
|
||||||
|
|
||||||
from textual.app import App
|
from textual.app import App
|
||||||
|
|
||||||
class RelativePathObjectApp(App[None]):
|
APP_DIR = Path(__file__).parent
|
||||||
|
|
||||||
|
|
||||||
|
class RelativePathObjectApp(App[None]):
|
||||||
CSS_PATH = Path("test.css")
|
CSS_PATH = Path("test.css")
|
||||||
|
|
||||||
class RelativePathStrApp(App[None]):
|
|
||||||
|
|
||||||
|
class RelativePathStrApp(App[None]):
|
||||||
CSS_PATH = "test.css"
|
CSS_PATH = "test.css"
|
||||||
|
|
||||||
class AbsolutePathObjectApp(App[None]):
|
|
||||||
|
|
||||||
|
class AbsolutePathObjectApp(App[None]):
|
||||||
CSS_PATH = Path("/tmp/test.css")
|
CSS_PATH = Path("/tmp/test.css")
|
||||||
|
|
||||||
class AbsolutePathStrApp(App[None]):
|
|
||||||
|
|
||||||
|
class AbsolutePathStrApp(App[None]):
|
||||||
CSS_PATH = "/tmp/test.css"
|
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():
|
class ListPathApp(App[None]):
|
||||||
path_tester(RelativePathObjectApp, RelativePathStrApp, ((Path(__file__).absolute().parent ) / "test.css").absolute())
|
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]
|
||||||
|
|||||||
Reference in New Issue
Block a user