Skip to content

Commit

Permalink
Merge pull request #2128 from Textualize/fix-2063
Browse files Browse the repository at this point in the history
Fix issues with watching CSS reloading
  • Loading branch information
rodrigogiraoserrao authored Mar 28, 2023
2 parents eaca92b + a06b59f commit 0189918
Show file tree
Hide file tree
Showing 8 changed files with 222 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

- Issue with parsing action strings whose arguments contained quoted closing parenthesis https://github.com/Textualize/textual/pull/2112
- Issues with parsing action strings with tuple arguments https://github.com/Textualize/textual/pull/2112
- Issue with watching for CSS file changes https://github.com/Textualize/textual/pull/2128
- Fix for tabs not invalidating https://github.com/Textualize/textual/issues/2125
- Fixed scrollbar layers issue https://github.com/Textualize/textual/issues/1358
- Fix for interaction between pseudo-classes and widget-level render caches https://github.com/Textualize/textual/pull/2155
Expand Down
16 changes: 12 additions & 4 deletions src/textual/_doc.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from __future__ import annotations

import hashlib
import inspect
import os
import shlex
from pathlib import Path
from typing import Iterable, cast
from typing import Awaitable, Callable, Iterable, cast

from textual._import_app import import_app
from textual.app import App
Expand Down Expand Up @@ -54,6 +55,7 @@ def take_svg_screenshot(
press: Iterable[str] = (),
title: str | None = None,
terminal_size: tuple[int, int] = (80, 24),
run_before: Callable[[Pilot], Awaitable[None] | None] | None = None,
) -> str:
"""
Expand All @@ -63,11 +65,13 @@ def take_svg_screenshot(
press: Key presses to run before taking screenshot. "_" is a short pause.
title: The terminal title in the output image.
terminal_size: A pair of integers (rows, columns), representing terminal size.
run_before: An arbitrary callable that runs arbitrary code before taking the
screenshot. Use this to simulate complex user interactions with the app
that cannot be simulated by key presses.
Returns:
An SVG string, showing the content of the terminal window at the time
the screenshot was taken.
"""

if app is None:
Expand All @@ -90,7 +94,7 @@ def get_cache_key(app: App) -> str:
cache_key = f"{hash.hexdigest()}.svg"
return cache_key

if app_path is not None:
if app_path is not None and run_before is None:
screenshot_cache = Path(SCREENSHOT_CACHE)
screenshot_cache.mkdir(exist_ok=True)

Expand All @@ -100,6 +104,10 @@ def get_cache_key(app: App) -> str:

async def auto_pilot(pilot: Pilot) -> None:
app = pilot.app
if run_before is not None:
result = run_before(pilot)
if inspect.isawaitable(result):
await result
await pilot.press(*press)
await pilot.wait_for_scheduled_animations()
await pilot.pause()
Expand All @@ -116,7 +124,7 @@ async def auto_pilot(pilot: Pilot) -> None:
),
)

if app_path is not None:
if app_path is not None and run_before is None:
screenshot_path.write_text(svg)

assert svg is not None
Expand Down
1 change: 0 additions & 1 deletion src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1081,7 +1081,6 @@ async def _on_css_change(self) -> None:
self.bell()
else:
self.stylesheet = stylesheet
self.reset_styles()
self.stylesheet.update(self)
self.screen.refresh(layout=True)

Expand Down
155 changes: 155 additions & 0 deletions tests/snapshot_tests/__snapshots__/test_snapshots.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -1985,6 +1985,161 @@

'''
# ---
# name: test_css_hot_reloading
'''
<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-3709538693-matrix {
font-family: Fira Code, monospace;
font-size: 20px;
line-height: 24.4px;
font-variant-east-asian: full-width;
}

.terminal-3709538693-title {
font-size: 18px;
font-weight: bold;
font-family: arial;
}

.terminal-3709538693-r1 { fill: #e1e1e1 }
.terminal-3709538693-r2 { fill: #c5c8c6 }
</style>

<defs>
<clipPath id="terminal-3709538693-clip-terminal">
<rect x="0" y="0" width="975.0" height="584.5999999999999" />
</clipPath>
<clipPath id="terminal-3709538693-line-0">
<rect x="0" y="1.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-3709538693-line-1">
<rect x="0" y="25.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-3709538693-line-2">
<rect x="0" y="50.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-3709538693-line-3">
<rect x="0" y="74.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-3709538693-line-4">
<rect x="0" y="99.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-3709538693-line-5">
<rect x="0" y="123.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-3709538693-line-6">
<rect x="0" y="147.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-3709538693-line-7">
<rect x="0" y="172.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-3709538693-line-8">
<rect x="0" y="196.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-3709538693-line-9">
<rect x="0" y="221.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-3709538693-line-10">
<rect x="0" y="245.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-3709538693-line-11">
<rect x="0" y="269.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-3709538693-line-12">
<rect x="0" y="294.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-3709538693-line-13">
<rect x="0" y="318.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-3709538693-line-14">
<rect x="0" y="343.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-3709538693-line-15">
<rect x="0" y="367.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-3709538693-line-16">
<rect x="0" y="391.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-3709538693-line-17">
<rect x="0" y="416.3" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-3709538693-line-18">
<rect x="0" y="440.7" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-3709538693-line-19">
<rect x="0" y="465.1" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-3709538693-line-20">
<rect x="0" y="489.5" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-3709538693-line-21">
<rect x="0" y="513.9" width="976" height="24.65"/>
</clipPath>
<clipPath id="terminal-3709538693-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-3709538693-title" fill="#c5c8c6" text-anchor="middle" x="496" y="27">HotReloadingApp</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-3709538693-clip-terminal)">
<rect fill="#1e1e1e" x="0" y="1.5" width="158.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="158.6" y="1.5" width="817.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#1e1e1e" x="0" y="25.9" width="976" 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-3709538693-matrix">
<text class="terminal-3709538693-r1" x="0" y="20" textLength="158.6" clip-path="url(#terminal-3709538693-line-0)">Hello,&#160;world!</text><text class="terminal-3709538693-r2" x="976" y="20" textLength="12.2" clip-path="url(#terminal-3709538693-line-0)">
</text><text class="terminal-3709538693-r2" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-3709538693-line-1)">
</text><text class="terminal-3709538693-r2" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-3709538693-line-2)">
</text><text class="terminal-3709538693-r2" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-3709538693-line-3)">
</text><text class="terminal-3709538693-r2" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-3709538693-line-4)">
</text><text class="terminal-3709538693-r2" x="976" y="142" textLength="12.2" clip-path="url(#terminal-3709538693-line-5)">
</text><text class="terminal-3709538693-r2" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-3709538693-line-6)">
</text><text class="terminal-3709538693-r2" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-3709538693-line-7)">
</text><text class="terminal-3709538693-r2" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-3709538693-line-8)">
</text><text class="terminal-3709538693-r2" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-3709538693-line-9)">
</text><text class="terminal-3709538693-r2" x="976" y="264" textLength="12.2" clip-path="url(#terminal-3709538693-line-10)">
</text><text class="terminal-3709538693-r2" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-3709538693-line-11)">
</text><text class="terminal-3709538693-r2" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-3709538693-line-12)">
</text><text class="terminal-3709538693-r2" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-3709538693-line-13)">
</text><text class="terminal-3709538693-r2" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-3709538693-line-14)">
</text><text class="terminal-3709538693-r2" x="976" y="386" textLength="12.2" clip-path="url(#terminal-3709538693-line-15)">
</text><text class="terminal-3709538693-r2" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-3709538693-line-16)">
</text><text class="terminal-3709538693-r2" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-3709538693-line-17)">
</text><text class="terminal-3709538693-r2" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-3709538693-line-18)">
</text><text class="terminal-3709538693-r2" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-3709538693-line-19)">
</text><text class="terminal-3709538693-r2" x="976" y="508" textLength="12.2" clip-path="url(#terminal-3709538693-line-20)">
</text><text class="terminal-3709538693-r2" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-3709538693-line-21)">
</text><text class="terminal-3709538693-r2" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-3709538693-line-22)">
</text>
</g>
</g>
</svg>

'''
# ---
# name: test_css_property[align.py]
'''
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
Expand Down
10 changes: 8 additions & 2 deletions tests/snapshot_tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from operator import attrgetter
from os import PathLike
from pathlib import Path, PurePath
from typing import Union, List, Optional, Callable, Iterable
from typing import Awaitable, Union, List, Optional, Callable, Iterable

import pytest
from _pytest.config import ExitCode
Expand All @@ -21,6 +21,7 @@
from textual._doc import take_svg_screenshot
from textual._import_app import import_app
from textual.app import App
from textual.pilot import Pilot

TEXTUAL_SNAPSHOT_SVG_KEY = pytest.StashKey[str]()
TEXTUAL_ACTUAL_SVG_KEY = pytest.StashKey[str]()
Expand All @@ -42,6 +43,7 @@ def compare(
app_path: str | PurePath,
press: Iterable[str] = ("_",),
terminal_size: tuple[int, int] = (80, 24),
run_before: Callable[[Pilot], Awaitable[None] | None] | None = None,
) -> bool:
"""
Compare a current screenshot of the app running at app_path, with
Expand All @@ -54,9 +56,12 @@ def compare(
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.
run_before: An arbitrary callable that runs arbitrary code before taking the
screenshot. Use this to simulate complex user interactions with the app
that cannot be simulated by key presses.
Returns:
bool: True if the screenshot matches the snapshot.
Whether the screenshot matches the snapshot.
"""
node = request.node
path = Path(app_path)
Expand All @@ -74,6 +79,7 @@ def compare(
app=app,
press=press,
terminal_size=terminal_size,
run_before=run_before,
)
result = snapshot == actual_screenshot

Expand Down
1 change: 1 addition & 0 deletions tests/snapshot_tests/snapshot_apps/hot_reloading_app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/* This file is purposefully empty. */
31 changes: 31 additions & 0 deletions tests/snapshot_tests/snapshot_apps/hot_reloading_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from pathlib import Path

from textual.app import App, ComposeResult
from textual.containers import Container
from textual.widgets import Label


CSS_PATH = (Path(__file__) / "../hot_reloading_app.css").resolve()

# Write some CSS to the file before the app loads.
# Then, the test will clear all the CSS to see if the
# hot reloading applies the changes correctly.
CSS_PATH.write_text(
"""
Container {
align: center middle;
}
Label {
border: round $primary;
padding: 3;
}
"""
)


class HotReloadingApp(App[None]):
CSS_PATH = CSS_PATH

def compose(self) -> ComposeResult:
yield Container(Label("Hello, world!"))
14 changes: 14 additions & 0 deletions tests/snapshot_tests/test_snapshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,20 @@ def test_scrollbar_thumb_height(snap_compare):
)


def test_css_hot_reloading(snap_compare):
"""Regression test for https://github.com/Textualize/textual/issues/2063."""

async def run_before(pilot):
css_file = pilot.app.CSS_PATH
with open(css_file, "w") as f:
f.write("/* This file is purposefully empty. */\n") # Clear all the CSS.
await pilot.app._on_css_change()

assert snap_compare(
SNAPSHOT_APPS_DIR / "hot_reloading_app.py", run_before=run_before
)


def test_layer_fix(snap_compare):
# Check https://github.com/Textualize/textual/issues/1358
assert snap_compare(SNAPSHOT_APPS_DIR / "layer_fix.py", press=["d"])

0 comments on commit 0189918

Please sign in to comment.