mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Implement a Progress Bar widget. (#2333)
* First prototype of PB.
* Repurpose UnderlineBar.
* Factor out 'Bar' widget.
* Revert "Factor out 'Bar' widget."
This reverts commit 0bb4871adf.
* Add Bar widget.
* Cap progress at 100%.
* Add skeleton for the ETA label.
[skip ci]
* Add ETA display.
* Improve docstrings.
* Directly compute percentage.
* Watch percentage changes directly.
[skip ci]
* Documentation.
* Make reactive percentage private.
Instead, we create a public read-only percentage property.
* Update griffe to fix documentation issue.
Related issues: #1572, https://github.com/mkdocstrings/griffe/issues/128.
Related PRs: https://github.com/mkdocstrings/griffe/pull/135.
* Add example and docs.
* Address review feedback.
[skip ci]
* More documentation.
* Add tests.
* Changelog.
* More tests.
* Fix/fake tests.
* Final tweaks.
This commit is contained in:
committed by
GitHub
parent
ee0d407067
commit
4148b1d450
@@ -4,7 +4,7 @@ from rich.console import Console, ConsoleOptions
|
||||
from rich.text import Text
|
||||
|
||||
from tests.utilities.render import render
|
||||
from textual.renderables.underline_bar import UnderlineBar
|
||||
from textual.renderables.bar import Bar
|
||||
|
||||
MAGENTA = "\x1b[35m"
|
||||
GREY = "\x1b[38;5;59m"
|
||||
@@ -14,22 +14,22 @@ RED = "\x1b[31m"
|
||||
|
||||
|
||||
def test_no_highlight():
|
||||
bar = UnderlineBar(width=6)
|
||||
bar = Bar(width=6)
|
||||
assert render(bar) == f"{GREY}━━━━━━{STOP}"
|
||||
|
||||
|
||||
def test_highlight_from_zero():
|
||||
bar = UnderlineBar(highlight_range=(0, 2.5), width=6)
|
||||
bar = Bar(highlight_range=(0, 2.5), width=6)
|
||||
assert render(bar) == (f"{MAGENTA}━━{STOP}{MAGENTA}╸{STOP}{GREY}━━━{STOP}")
|
||||
|
||||
|
||||
def test_highlight_from_zero_point_five():
|
||||
bar = UnderlineBar(highlight_range=(0.5, 2), width=6)
|
||||
bar = Bar(highlight_range=(0.5, 2), width=6)
|
||||
assert render(bar) == (f"{MAGENTA}╺━{STOP}{GREY}╺{STOP}{GREY}━━━{STOP}")
|
||||
|
||||
|
||||
def test_highlight_middle():
|
||||
bar = UnderlineBar(highlight_range=(2, 4), width=6)
|
||||
bar = Bar(highlight_range=(2, 4), width=6)
|
||||
assert render(bar) == (
|
||||
f"{GREY}━{STOP}"
|
||||
f"{GREY}╸{STOP}"
|
||||
@@ -40,14 +40,14 @@ def test_highlight_middle():
|
||||
|
||||
|
||||
def test_highlight_half_start():
|
||||
bar = UnderlineBar(highlight_range=(2.5, 4), width=6)
|
||||
bar = Bar(highlight_range=(2.5, 4), width=6)
|
||||
assert render(bar) == (
|
||||
f"{GREY}━━{STOP}" f"{MAGENTA}╺━{STOP}" f"{GREY}╺{STOP}" f"{GREY}━{STOP}"
|
||||
)
|
||||
|
||||
|
||||
def test_highlight_half_end():
|
||||
bar = UnderlineBar(highlight_range=(2, 4.5), width=6)
|
||||
bar = Bar(highlight_range=(2, 4.5), width=6)
|
||||
assert render(bar) == (
|
||||
f"{GREY}━{STOP}"
|
||||
f"{GREY}╸{STOP}"
|
||||
@@ -58,46 +58,46 @@ def test_highlight_half_end():
|
||||
|
||||
|
||||
def test_highlight_half_start_and_half_end():
|
||||
bar = UnderlineBar(highlight_range=(2.5, 4.5), width=6)
|
||||
bar = Bar(highlight_range=(2.5, 4.5), width=6)
|
||||
assert render(bar) == (
|
||||
f"{GREY}━━{STOP}" f"{MAGENTA}╺━{STOP}" f"{MAGENTA}╸{STOP}" f"{GREY}━{STOP}"
|
||||
)
|
||||
|
||||
|
||||
def test_highlight_to_near_end():
|
||||
bar = UnderlineBar(highlight_range=(3, 5.5), width=6)
|
||||
bar = Bar(highlight_range=(3, 5.5), width=6)
|
||||
assert render(bar) == (
|
||||
f"{GREY}━━{STOP}" f"{GREY}╸{STOP}" f"{MAGENTA}━━{STOP}" f"{MAGENTA}╸{STOP}"
|
||||
)
|
||||
|
||||
|
||||
def test_highlight_to_end():
|
||||
bar = UnderlineBar(highlight_range=(3, 6), width=6)
|
||||
bar = Bar(highlight_range=(3, 6), width=6)
|
||||
assert render(bar) == (f"{GREY}━━{STOP}{GREY}╸{STOP}{MAGENTA}━━━{STOP}")
|
||||
|
||||
|
||||
def test_highlight_out_of_bounds_start():
|
||||
bar = UnderlineBar(highlight_range=(-2, 3), width=6)
|
||||
bar = Bar(highlight_range=(-2, 3), width=6)
|
||||
assert render(bar) == (f"{MAGENTA}━━━{STOP}{GREY}╺{STOP}{GREY}━━{STOP}")
|
||||
|
||||
|
||||
def test_highlight_out_of_bounds_end():
|
||||
bar = UnderlineBar(highlight_range=(3, 9), width=6)
|
||||
bar = Bar(highlight_range=(3, 9), width=6)
|
||||
assert render(bar) == (f"{GREY}━━{STOP}{GREY}╸{STOP}{MAGENTA}━━━{STOP}")
|
||||
|
||||
|
||||
def test_highlight_full_range_out_of_bounds_end():
|
||||
bar = UnderlineBar(highlight_range=(9, 10), width=6)
|
||||
bar = Bar(highlight_range=(9, 10), width=6)
|
||||
assert render(bar) == f"{GREY}━━━━━━{STOP}"
|
||||
|
||||
|
||||
def test_highlight_full_range_out_of_bounds_start():
|
||||
bar = UnderlineBar(highlight_range=(-5, -2), width=6)
|
||||
bar = Bar(highlight_range=(-5, -2), width=6)
|
||||
assert render(bar) == f"{GREY}━━━━━━{STOP}"
|
||||
|
||||
|
||||
def test_custom_styles():
|
||||
bar = UnderlineBar(
|
||||
bar = Bar(
|
||||
highlight_range=(2, 4), highlight_style="red", background_style="green", width=6
|
||||
)
|
||||
assert render(bar) == (
|
||||
@@ -110,7 +110,7 @@ def test_custom_styles():
|
||||
|
||||
|
||||
def test_clickable_ranges():
|
||||
bar = UnderlineBar(
|
||||
bar = Bar(
|
||||
highlight_range=(0, 1), width=6, clickable_ranges={"foo": (0, 2), "bar": (4, 5)}
|
||||
)
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -203,6 +203,30 @@ def test_option_list(snap_compare):
|
||||
assert snap_compare(WIDGET_EXAMPLES_DIR / "option_list_tables.py")
|
||||
|
||||
|
||||
def test_progress_bar_indeterminate(snap_compare):
|
||||
assert snap_compare(WIDGET_EXAMPLES_DIR / "progress_bar_isolated_.py", press=["f"])
|
||||
|
||||
|
||||
def test_progress_bar_indeterminate_styled(snap_compare):
|
||||
assert snap_compare(WIDGET_EXAMPLES_DIR / "progress_bar_styled_.py", press=["f"])
|
||||
|
||||
|
||||
def test_progress_bar_halfway(snap_compare):
|
||||
assert snap_compare(WIDGET_EXAMPLES_DIR / "progress_bar_isolated_.py", press=["t"])
|
||||
|
||||
|
||||
def test_progress_bar_halfway_styled(snap_compare):
|
||||
assert snap_compare(WIDGET_EXAMPLES_DIR / "progress_bar_styled_.py", press=["t"])
|
||||
|
||||
|
||||
def test_progress_bar_completed(snap_compare):
|
||||
assert snap_compare(WIDGET_EXAMPLES_DIR / "progress_bar_isolated_.py", press=["u"])
|
||||
|
||||
|
||||
def test_progress_bar_completed_styled(snap_compare):
|
||||
assert snap_compare(WIDGET_EXAMPLES_DIR / "progress_bar_styled_.py", press=["u"])
|
||||
|
||||
|
||||
# --- CSS properties ---
|
||||
# We have a canonical example for each CSS property that is shown in their docs.
|
||||
# If any of these change, something has likely broken, so snapshot each of them.
|
||||
|
||||
165
tests/test_progress_bar.py
Normal file
165
tests/test_progress_bar.py
Normal file
@@ -0,0 +1,165 @@
|
||||
import pytest
|
||||
from pytest import approx
|
||||
|
||||
from textual.app import App
|
||||
from textual.css.query import NoMatches
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import ProgressBar
|
||||
|
||||
|
||||
def test_initial_status():
|
||||
pb = ProgressBar()
|
||||
assert pb.total is None
|
||||
assert pb.progress == 0
|
||||
assert pb.percentage is None
|
||||
|
||||
pb = ProgressBar(total=100)
|
||||
assert pb.total == 100
|
||||
assert pb.progress == 0
|
||||
assert pb.percentage == 0
|
||||
|
||||
|
||||
def test_advance():
|
||||
pb = ProgressBar(total=100)
|
||||
|
||||
pb.advance(10)
|
||||
assert pb.progress == 10
|
||||
assert pb.percentage == approx(0.1)
|
||||
|
||||
pb.advance(42)
|
||||
assert pb.progress == 52
|
||||
assert pb.percentage == approx(0.52)
|
||||
|
||||
pb.advance(0.0625)
|
||||
assert pb.progress == 52.0625
|
||||
assert pb.percentage == approx(0.520625)
|
||||
|
||||
|
||||
def test_advance_backwards():
|
||||
pb = ProgressBar(total=100)
|
||||
|
||||
pb.progress = 50
|
||||
|
||||
pb.advance(-10)
|
||||
assert pb.progress == 40
|
||||
|
||||
|
||||
def test_progress_overflow():
|
||||
pb = ProgressBar(total=100)
|
||||
|
||||
pb.advance(999_999)
|
||||
assert pb.progress == 100
|
||||
assert pb.percentage == 1
|
||||
|
||||
pb.update(total=50)
|
||||
assert pb.progress == 50
|
||||
assert pb.percentage == 1
|
||||
|
||||
|
||||
def test_progress_underflow():
|
||||
pb = ProgressBar(total=100)
|
||||
|
||||
pb.advance(-999_999)
|
||||
assert pb.progress == 0
|
||||
assert pb.percentage == 0
|
||||
|
||||
|
||||
def test_non_negative_total():
|
||||
pb = ProgressBar(total=-100)
|
||||
assert pb.total == 0
|
||||
|
||||
|
||||
def test_update_total():
|
||||
pb = ProgressBar()
|
||||
|
||||
pb.update(total=100)
|
||||
assert pb.total == 100
|
||||
|
||||
pb.update(total=1000)
|
||||
assert pb.total == 1000
|
||||
|
||||
pb.update(total=None)
|
||||
assert pb.total == 1000
|
||||
|
||||
pb.update(total=100)
|
||||
assert pb.total == 100
|
||||
|
||||
|
||||
def test_update_progress():
|
||||
pb = ProgressBar(total=100)
|
||||
|
||||
pb.update(progress=10)
|
||||
assert pb.progress == 10
|
||||
|
||||
pb.update(progress=73)
|
||||
assert pb.progress == 73
|
||||
|
||||
pb.update(progress=40)
|
||||
assert pb.progress == 40
|
||||
|
||||
|
||||
def test_update_advance():
|
||||
pb = ProgressBar(total=100)
|
||||
|
||||
pb.update(advance=10)
|
||||
assert pb.progress == 10
|
||||
|
||||
pb.update(advance=10)
|
||||
assert pb.progress == 20
|
||||
|
||||
pb.update(advance=10)
|
||||
assert pb.progress == 30
|
||||
|
||||
|
||||
def test_update():
|
||||
pb = ProgressBar()
|
||||
|
||||
pb.update(total=100, progress=30, advance=20)
|
||||
assert pb.total == 100
|
||||
assert pb.progress == 50
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
["show_bar", "show_percentage", "show_eta"],
|
||||
[
|
||||
(True, True, True),
|
||||
(True, True, False),
|
||||
(True, False, True),
|
||||
(True, False, False),
|
||||
(False, True, True),
|
||||
(False, True, False),
|
||||
(False, False, True),
|
||||
(False, False, False),
|
||||
],
|
||||
)
|
||||
async def test_show_sub_widgets(show_bar: bool, show_percentage: bool, show_eta: bool):
|
||||
class PBApp(App[None]):
|
||||
def compose(self):
|
||||
self.pb = ProgressBar(
|
||||
show_bar=show_bar, show_percentage=show_percentage, show_eta=show_eta
|
||||
)
|
||||
yield self.pb
|
||||
|
||||
app = PBApp()
|
||||
|
||||
async with app.run_test():
|
||||
if show_bar:
|
||||
bar = app.pb.query_one("#bar")
|
||||
assert isinstance(bar, Widget)
|
||||
else:
|
||||
with pytest.raises(NoMatches):
|
||||
app.pb.query_one("#bar")
|
||||
|
||||
if show_percentage:
|
||||
percentage = app.pb.query_one("#percentage")
|
||||
assert isinstance(percentage, Widget)
|
||||
else:
|
||||
with pytest.raises(NoMatches):
|
||||
app.pb.query_one("#percentage")
|
||||
|
||||
if show_eta:
|
||||
eta = app.pb.query_one("#eta")
|
||||
assert isinstance(eta, Widget)
|
||||
else:
|
||||
with pytest.raises(NoMatches):
|
||||
app.pb.query_one("#eta")
|
||||
Reference in New Issue
Block a user