diff --git a/docs/examples/styles/grid.py b/docs/examples/styles/grid.py index 338af444c..1901d1862 100644 --- a/docs/examples/styles/grid.py +++ b/docs/examples/styles/grid.py @@ -14,5 +14,3 @@ class GridApp(App): app = GridApp(css_path="grid.css") -if __name__ == "__main__": - app.run() diff --git a/docs/examples/styles/links.py b/docs/examples/styles/links.py index cf45563cc..ddd472974 100644 --- a/docs/examples/styles/links.py +++ b/docs/examples/styles/links.py @@ -13,6 +13,3 @@ class LinksApp(App): app = LinksApp(css_path="links.css") - -if __name__ == "__main__": - app.run() diff --git a/docs/examples/widgets/checkbox.py b/docs/examples/widgets/checkbox.py index ff2b27196..400f2ae25 100644 --- a/docs/examples/widgets/checkbox.py +++ b/docs/examples/widgets/checkbox.py @@ -7,7 +7,9 @@ class CheckboxApp(App): def compose(self) -> ComposeResult: yield Static("[b]Example checkboxes\n", classes="label") yield Horizontal( - Static("off: ", classes="label"), Checkbox(), classes="container" + Static("off: ", classes="label"), + Checkbox(animate=False), + classes="container", ) yield Horizontal( Static("on: ", classes="label"), diff --git a/docs/examples/widgets/table.py b/docs/examples/widgets/data_table.py similarity index 100% rename from docs/examples/widgets/table.py rename to docs/examples/widgets/data_table.py diff --git a/docs/examples/widgets/footer.py b/docs/examples/widgets/footer.py index 47d9c9aa6..85406b4e9 100644 --- a/docs/examples/widgets/footer.py +++ b/docs/examples/widgets/footer.py @@ -4,7 +4,16 @@ from textual.widgets import Footer class FooterApp(App): - BINDINGS = [Binding(key="q", action="quit", description="Quit the app")] + BINDINGS = [ + Binding(key="q", action="quit", description="Quit the app"), + Binding( + key="question_mark", + action="help", + description="Show help screen", + key_display="?", + ), + Binding(key="j", action="down", description="Scroll down", show=False), + ] def compose(self) -> ComposeResult: yield Footer() diff --git a/docs/widgets/data_table.md b/docs/widgets/data_table.md index ce7f25bf5..7d5558a95 100644 --- a/docs/widgets/data_table.md +++ b/docs/widgets/data_table.md @@ -11,13 +11,13 @@ The example below populates a table with CSV data. === "Output" - ```{.textual path="docs/examples/widgets/table.py"} + ```{.textual path="docs/examples/widgets/data_table.py"} ``` -=== "table.py" +=== "data_table.py" ```python - --8<-- "docs/examples/widgets/table.py" + --8<-- "docs/examples/widgets/data_table.py" ``` diff --git a/src/textual/app.py b/src/textual/app.py index c044f6155..43f1bd56c 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -605,7 +605,7 @@ class App(Generic[ReturnType], DOMNode): assert press driver = app._driver assert driver is not None - await asyncio.sleep(0.01) + await asyncio.sleep(0.02) for key in press: if key == "_": print("(pause 50ms)") @@ -632,7 +632,13 @@ class App(Generic[ReturnType], DOMNode): print(f"press {key!r} (char={char!r})") key_event = events.Key(self, key, char) driver.send_event(key_event) - await asyncio.sleep(0.01) + # TODO: A bit of a fudge - extra sleep after tabbing to help guard against race + # condition between widget-level key handling and app/screen level handling. + # More information here: https://github.com/Textualize/textual/issues/1009 + # This conditional sleep can be removed after that issue is closed. + if key == "tab": + await asyncio.sleep(0.05) + await asyncio.sleep(0.02) await app._animator.wait_for_idle() diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index e5ffd0b4e..92eb75fc1 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -1,3 +1,189 @@ +# name: test_buttons_render + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ButtonsApp + + + + + + + + + + + Standard ButtonsDisabled Buttons + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  Default  Default  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  Primary!  Primary!  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  Success!  Success!  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  Warning!  Warning!  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ +  Error!  Error!  + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + ''' +# --- # name: test_checkboxes ''' @@ -21,136 +207,4391 @@ font-weight: 700; } - .terminal-900458690-matrix { + .terminal-1548740802-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-900458690-title { + .terminal-1548740802-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-900458690-r1 { fill: #e1e1e1 } - .terminal-900458690-r2 { fill: #c5c8c6 } - .terminal-900458690-r3 { fill: #e1e1e1;font-weight: bold } - .terminal-900458690-r4 { fill: #1e1e1e } - .terminal-900458690-r5 { fill: #e2e3e3 } - .terminal-900458690-r6 { fill: #0178d4 } - .terminal-900458690-r7 { fill: #e3e8e8 } + .terminal-1548740802-r1 { fill: #e1e1e1 } + .terminal-1548740802-r2 { fill: #c5c8c6 } + .terminal-1548740802-r3 { fill: #e1e1e1;font-weight: bold } + .terminal-1548740802-r4 { fill: #1e1e1e } + .terminal-1548740802-r5 { fill: #0178d4 } + .terminal-1548740802-r6 { fill: #e2e3e3 } + .terminal-1548740802-r7 { fill: #e3e8e8 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - CheckboxApp + CheckboxApp - - - - - - - - Example checkboxes - - - ▔▔▔▔▔▔▔▔ - off:      - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - on:       - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - focused:  - ▁▁▁▁▁▁▁▁ - ▔▔▔▔▔▔▔▔ - custom:   - ▁▁▁▁▁▁▁▁ - - - - + + + + + + + + Example checkboxes + + + ▔▔▔▔▔▔▔▔ + off:      + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + on:       + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + focused:  + ▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔ + custom:   + ▁▁▁▁▁▁▁▁ + + + + + + + + + + ''' +# --- +# name: test_css_property_snapshot[docs/examples/styles/align.py] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AlignApp + + + + + + + + + + + + + + + + ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + + Vertical alignment with Textual + + ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + + ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + + Take note, browsers. + + ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + + + + + + + + + + + + ''' +# --- +# name: test_css_property_snapshot[docs/examples/styles/background.py] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BackgroundApp + + + + + + + + + + + + + Widget 1 + + + + + + + + Widget 2 + + + + + + + + Widget 3 + + + + + + + + + ''' +# --- +# name: test_css_property_snapshot[docs/examples/styles/border.py] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BorderApp + + + + + + + + + + + ┌────────────────────────────────────────────────────────────────────────────┐ + + My border is solid red + + └────────────────────────────────────────────────────────────────────────────┘ + + ┏╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍┓ + + My border is dashed green + + ┗╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍┛ + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + My border is tall blue + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + ''' +# --- +# name: test_css_property_snapshot[docs/examples/styles/box_sizing.py] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BoxSizingApp + + + + + + + + + + + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + I'm using border-box! + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + I'm using content-box! + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + + + + + + + + + + ''' +# --- +# name: test_css_property_snapshot[docs/examples/styles/color.py] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ColorApp + + + + + + + + + + + + + I'm red! + + + + + + + + I'm rgb(0, 255, 0)! + + + + + + + + I'm hsl(240, 100%, 50%)! + + + + + + + + + ''' +# --- +# name: test_css_property_snapshot[docs/examples/styles/content_align.py] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ContentAlignApp + + + + + + + + + + + With content-align you can... + + + + + + + + + + ...Easily align content... + + + + + + + + + + + ...Horizontally and vertically! + + + + + + ''' +# --- +# name: test_css_property_snapshot[docs/examples/styles/display.py] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DisplayApp + + + + + + + + + + ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃Widget 1 + + + ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃Widget 3 + + + ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_css_property_snapshot[docs/examples/styles/grid.py] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GridApp + + + + + + + + + + + Grid cell 1Grid cell 2 + + row-span: 3; + column-span: 2; + + + Grid cell 3 + + + + + + Grid cell 4 + + + + + + Grid cell 5Grid cell 6Grid cell 7 + + + + + + + + + ''' +# --- +# name: test_css_property_snapshot[docs/examples/styles/height.py] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + HeightApp + + + + + + + + + + Widget + + + + + + + + + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_css_property_snapshot[docs/examples/styles/layout.py] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LayoutApp + + + + + + + + + + + Layout + + Is + + Vertical + + + LayoutIsHorizontal + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_css_property_snapshot[docs/examples/styles/links.py] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LinksApp + + + + + + + + + + Here is a link which you can click! + + Here is a link which you can click! + + + + + + + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_css_property_snapshot[docs/examples/styles/margin.py] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MarginApp + + + + + + + + + + + + + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + I must not fear. + Fear is the mind-killer. + Fear is the little-death that brings total obliteration. + I will face my fear. + I will permit it to pass over me and through me. + And when it has gone past, I will turn the inner eye to see  + its path. + Where the fear has gone there will be nothing. Only I will  + remain. + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + + + + + + + + + + + + + ''' +# --- +# name: test_css_property_snapshot[docs/examples/styles/offset.py] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OffsetApp + + + + + + + + + + + + ▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜ + + + ▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜ + Paul (offset 8 2) + + + Chani (offset 0 5)▐ + ▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ + ▙▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ + + + Duncan (offset ▙▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▟ + 10) + + + + ▙▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▟ + + + + + + + + + ''' +# --- +# name: test_css_property_snapshot[docs/examples/styles/opacity.py] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OpacityApp + + + + + + + + + + + + + + ▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜ + + opacity: 25% + + ▙▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▟ + ▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜ + + opacity: 50% + + ▙▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▟ + ▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜ + + opacity: 75% + + ▙▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▟ + ▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜ + + opacity: 100% + + ▙▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▟ + + + + + ''' +# --- +# name: test_css_property_snapshot[docs/examples/styles/outline.py] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OutlineApp + + + + + + + + + + + + + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ear is the mind-killer. + ear is the little-death that brings total obliteration. +  will face my fear. +  will permit it to pass over me and through me. + nd when it has gone past, I will turn the inner eye to see its + ath. + here the fear has gone there will be nothing. Only I will  + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + + + + + + + + + + + + + + + ''' +# --- +# name: test_css_property_snapshot[docs/examples/styles/overflow.py] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OverflowApp + + + + + + + + + + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + I must not fear.I must not fear. + Fear is the mind-killer.Fear is the mind-killer. + Fear is the little-death that Fear is the little-death that  + brings total obliteration.brings total obliteration. + I will face my fear.I will face my fear. + I will permit it to pass over meI will permit it to pass over me  + and through me.and through me. + And when it has gone past, I And when it has gone past, I will  + will turn the inner eye to see turn the inner eye to see its  + its path.▁▁path. + Where the fear has gone there Where the fear has gone there will + will be nothing. Only I will be nothing. Only I will remain. + remain.▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁I must not fear. + I must not fear.Fear is the mind-killer. + Fear is the mind-killer.Fear is the little-death that  + Fear is the little-death that brings total obliteration. + brings total obliteration.I will face my fear. + I will face my fear.I will permit it to pass over me  + I will permit it to pass over meand through me. + + + + + ''' +# --- +# name: test_css_property_snapshot[docs/examples/styles/padding.py] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PaddingApp + + + + + + + + + + + + + + I must not fear. + Fear is the mind-killer. + Fear is the little-death that brings total obliteration. + I will face my fear. + I will permit it to pass over me and through me. + And when it has gone past, I will turn the inner eye to see its  + path. + Where the fear has gone there will be nothing. Only I will  + remain. + + + + + + + + + + + + + + + + ''' +# --- +# name: test_css_property_snapshot[docs/examples/styles/scrollbar_gutter.py] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ScrollbarGutterApp + + + + + + + + + + I must not fear. + Fear is the mind-killer. + Fear is the little-death that brings total obliteration. + I will face my fear. + I will permit it to pass over me and through me. + And when it has gone past, I will turn the inner eye to see its path. + Where the fear has gone there will be nothing. Only I will remain. + + + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_css_property_snapshot[docs/examples/styles/scrollbar_size.py] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ScrollbarApp + + + + + + + + + + + + I must not fear. + Fear is the mind-killer. + Fear is the little-death that brings total obliteration. + I will face my fear. + I will permit it to pass over me and through me. + And when it has gone past, I will turn the inner eye to see its path. + Where the fear has gone there will be nothing. Only I will remain. + I must not fear. + Fear is the mind-killer. + Fear is the little-death that brings total obliteration. + I will face my fear. + I will permit it to pass over me and through me.▂▂▂▂ + And when it has gone past, I will turn the inner eye to see its path. + Where the fear has gone there will be nothing. Only I will remain. + I must not fear. + Fear is the mind-killer. + Fear is the little-death that brings total obliteration. + I will face my fear. + I will permit it to pass over me and through me. + And when it has gone past, I will turn the inner eye to see its path. + Where the fear has gone there will be nothing. Only I will remain. + + + + + + ''' +# --- +# name: test_css_property_snapshot[docs/examples/styles/scrollbars.py] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ScrollbarApp + + + + + + + + + + + + I must not fear.I must not fear. + Fear is the mind-killer.Fear is the mind-killer. + Fear is the little-death that Fear is the little-death that  + brings total obliteration.brings total obliteration. + I will face my fear.I will face my fear. + I will permit it to pass over I will permit it to pass over  + me and through me.▇▇me and through me.▇▇ + And when it has gone past, I And when it has gone past, I  + will turn the inner eye to seewill turn the inner eye to see + its path.its path. + Where the fear has gone there Where the fear has gone there  + will be nothing. Only I will will be nothing. Only I will  + remain.remain. + I must not fear.I must not fear. + Fear is the mind-killer.Fear is the mind-killer. + Fear is the little-death that Fear is the little-death that  + brings total obliteration.brings total obliteration. + I will face my fear.I will face my fear. + I will permit it to pass over I will permit it to pass over  + me and through me.me and through me. + And when it has gone past, I And when it has gone past, I  + + + + + + ''' +# --- +# name: test_css_property_snapshot[docs/examples/styles/text_align.py] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextAlign + + + + + + + + + + + Left aligned + I must not fear. Fear is the mind-killer. Fear is the little-death that brings + total obliteration. I will face my fear. I will permit it to pass over me and  + through me.                                                                    + + + Center aligned + I must not fear. Fear is the mind-killer. Fear is the little-death that brings + total obliteration. I will face my fear. I will permit it to pass over me and  +                                  through me.                                   + + + Right aligned + I must not fear. Fear is the mind-killer. Fear is the little-death that brings +  total obliteration. I will face my fear. I will permit it to pass over me and +                                                                    through me. + + + Justified + I must not fear. Fear is the mind-killer. Fear is the little-death that brings + total obliteration. I will face my fear. I will permit it to pass over me  and + through me. + + + + + + ''' +# --- +# name: test_css_property_snapshot[docs/examples/styles/text_opacity.py] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextOpacityApp + + + + + + + + + + + + + +                                text-opacity: 25%                                 + + + + +                                text-opacity: 50%                                 + + + + +                                text-opacity: 75%                                 + + + + +                                text-opacity: 100%                                + + + + + + + + + ''' +# --- +# name: test_css_property_snapshot[docs/examples/styles/text_style.py] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TextStyleApp + + + + + + + + + + I must not fear.I must not fear.I must not fear. + Fear is the mind-killer.Fear is the mind-killer.Fear is the mind-killer. + Fear is the little-death Fear is the little-death Fear is the little-death  + that brings total that brings total that brings total  + obliteration.obliteration.obliteration. + I will face my fear.I will face my fear.I will face my fear. + I will permit it to pass I will permit it to pass I will permit it to pass  + over me and through me.over me and through me.over me and through me. + And when it has gone past,And when it has gone past, And when it has gone past,  + I will turn the inner eye I will turn the inner eye I will turn the inner eye  + to see its path.to see its path.to see its path. + Where the fear has gone Where the fear has gone Where the fear has gone  + there will be nothing. there will be nothing. Onlythere will be nothing. Only + Only I will remain.I will remain.I will remain. + + + + + + + + + + + + + + + ''' +# --- +# name: test_css_property_snapshot[docs/examples/styles/tint.py] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TintApp + + + + + + + + + + + tint: green 0%; + + + tint: green 10%; + + + tint: green 20%; + + + tint: green 30%; + + + tint: green 40%; + + + tint: green 50%; + ▄▄ + + tint: green 60%; + + + tint: green 70%; + + + + + + ''' +# --- +# name: test_css_property_snapshot[docs/examples/styles/visibility.py] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + VisibilityApp + + + + + + + + + + ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃Widget 1 + + + ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + + + + + + ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃Widget 3 + + + ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + + + + + + + + + + + + + + ''' +# --- +# name: test_css_property_snapshot[docs/examples/styles/width.py] + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + WidthApp + + + + + + + + + + Widget + + + + + + + + + + + + + + + + + + + + + + + + + + + + ''' +# --- +# name: test_datatable_render + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TableApp + + + + + + + + + +  lane  swimmer               country        time   +  4     Joseph Schooling      Singapore      50.39  +  2     Michael Phelps        United States  51.14  +  5     Chad le Clos          South Africa   51.14  +  6     László Cseh           Hungary        51.14  +  3     Li Zhuhao             China          51.26  +  8     Mehdy Metella         France         51.58  +  7     Tom Shields           United States  51.73  +  1     Aleksandr Sadovnikov  Russia         51.84  + + + + + + + + + + + + + + @@ -315,6 +4756,163 @@ ''' # --- +# name: test_footer_render + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FooterApp + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +  Q  Quit the app  ?  Show help screen  + + + + + ''' +# --- # name: test_grid_layout_basic ''' @@ -783,6 +5381,162 @@ ''' # --- +# name: test_header_render + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + HeaderApp + + + + + + + + + + HeaderApp + + + + + + + + + + + + + + + + + + + + + + + + + + + + ''' +# --- # name: test_horizontal_layout ''' @@ -939,6 +5693,165 @@ ''' # --- +# name: test_input_and_focus + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + InputApp + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Darren                                                                     + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + Burns + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + + + + + + + + + + ''' +# --- # name: test_layers ''' diff --git a/tests/snapshot_tests/conftest.py b/tests/snapshot_tests/conftest.py index 79b5584b1..0a0f0d55c 100644 --- a/tests/snapshot_tests/conftest.py +++ b/tests/snapshot_tests/conftest.py @@ -59,6 +59,7 @@ def snap_compare( """ node = request.node app = import_app(app_path) + compare.app = app actual_screenshot = take_svg_screenshot( app=app, press=press, diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 72249c3ed..2f04c9314 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -1,3 +1,13 @@ +from pathlib import Path, PurePosixPath + +import pytest + +from textual.app import App +from textual.widgets import Input, Button + + +# --- Layout related stuff --- + def test_grid_layout_basic(snap_compare): assert snap_compare("docs/examples/guide/layout/grid_layout1.py") @@ -26,7 +36,73 @@ def test_dock_layout_sidebar(snap_compare): assert snap_compare("docs/examples/guide/layout/dock_layout2_sidebar.py") +# --- Widgets - rendering and basic interactions --- +# Each widget should have a canonical example that is display in the docs. +# When adding a new widget, ideally we should also create a snapshot test +# from these examples which test rendering and simple interactions with it. + def test_checkboxes(snap_compare): """Tests checkboxes but also acts a regression test for using width: auto in a Horizontal layout context.""" - assert snap_compare("docs/examples/widgets/checkbox.py") + press = [ + "shift+tab", + "enter", # toggle off + "shift+tab", + "wait:20", + "enter", # toggle on + "wait:20", + ] + assert snap_compare("docs/examples/widgets/checkbox.py", press=press) + + +def test_input_and_focus(snap_compare): + press = [ + "tab", + *"Darren", # Focus first input, write "Darren" + "tab", + *"Burns", # Tab focus to second input, write "Burns" + ] + assert snap_compare("docs/examples/widgets/input.py", press=press) + + # Assert that the state of the Input is what we'd expect + app: App = snap_compare.app + input: Input = app.query_one(Input) + assert input.value == "Darren" + assert input.cursor_position == 6 + assert input.view_position == 0 + + +def test_buttons_render(snap_compare): + # Testing button rendering. We press tab to focus the first button too. + assert snap_compare("docs/examples/widgets/button.py", press=["tab"]) + + app = snap_compare.app + button: Button = app.query_one(Button) + assert app.focused is button + + +def test_datatable_render(snap_compare): + press = ["tab", "down", "down", "right", "up", "left"] + assert snap_compare("docs/examples/widgets/data_table.py", press=press) + + +def test_footer_render(snap_compare): + assert snap_compare("docs/examples/widgets/footer.py") + + +def test_header_render(snap_compare): + assert snap_compare("docs/examples/widgets/header.py") + + +# --- 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. + +PATHS = [ + str(PurePosixPath(path)) for path in Path("docs/examples/styles").iterdir() if path.suffix == ".py" +] + + +@pytest.mark.parametrize("path", PATHS) +def test_css_property_snapshot(path, snap_compare): + assert snap_compare(path)