Add get_child_by_id and get_widget_by_id (#1146)

* Add get_child_by_id and get_widget_by_id

* Remove redundant code

* Add unit tests for app-level get_child_by_id and get_widget_by_id

* Remove redundant test fixture injection

* Update CHANGELOG

* Enforce uniqueness of ID amongst widget children

* Enforce unique widget IDs amongst widgets mounted together

* Update CHANGELOG.md

* Ensuring unique IDs in a more logical place

* Add docstring to NodeList._get_by_id

* Dont use duplicate IDs in tests, dont mount 2000 widgets

* Mounting less widgets in a unit test

* Reword error message

* Use lower-level depth first search in get_widget_by_id to break out early
This commit is contained in:
darrenburns
2022-11-16 15:29:59 +00:00
committed by GitHub
parent a465f5c236
commit df37a9b90a
10 changed files with 219 additions and 64 deletions

View File

@@ -1,7 +1,6 @@
import pytest
from textual.css.errors import StyleValueError
from textual.css.query import NoMatches
from textual.dom import DOMNode, BadIdentifier
@@ -26,37 +25,6 @@ def test_display_set_invalid_value():
node.display = "blah"
@pytest.fixture
def parent():
parent = DOMNode(id="parent")
child1 = DOMNode(id="child1")
child2 = DOMNode(id="child2")
grandchild1 = DOMNode(id="grandchild1")
child1._add_child(grandchild1)
parent._add_child(child1)
parent._add_child(child2)
yield parent
def test_get_child_gets_first_child(parent):
child = parent.get_child(id="child1")
assert child.id == "child1"
assert child.get_child(id="grandchild1").id == "grandchild1"
assert parent.get_child(id="child2").id == "child2"
def test_get_child_no_matching_child(parent):
with pytest.raises(NoMatches):
parent.get_child(id="doesnt-exist")
def test_get_child_only_immediate_descendents(parent):
with pytest.raises(NoMatches):
parent.get_child(id="grandchild1")
def test_validate():
with pytest.raises(BadIdentifier):
DOMNode(id="23")

View File

@@ -1,3 +1,5 @@
from __future__ import annotations
from textual.app import App, ComposeResult
from textual import events
from textual.containers import Container

View File

@@ -1,9 +1,13 @@
import pytest
import rich
from textual.app import App
from textual._node_list import DuplicateIds
from textual.app import App, ComposeResult
from textual.css.errors import StyleValueError
from textual.css.query import NoMatches
from textual.dom import DOMNode
from textual.geometry import Size
from textual.widget import Widget
from textual.widget import Widget, MountError
@pytest.mark.parametrize(
@@ -64,3 +68,92 @@ def test_widget_content_width():
height = widget3.get_content_height(Size(20, 20), Size(80, 24), width)
assert width == 3
assert height == 3
class GetByIdApp(App):
def compose(self) -> ComposeResult:
grandchild1 = Widget(id="grandchild1")
child1 = Widget(grandchild1, id="child1")
child2 = Widget(id="child2")
yield Widget(
child1,
child2,
id="parent",
)
@pytest.fixture
async def hierarchy_app():
app = GetByIdApp()
async with app.run_test():
yield app
@pytest.fixture
async def parent(hierarchy_app):
yield hierarchy_app.get_widget_by_id("parent")
def test_get_child_by_id_gets_first_child(parent):
child = parent.get_child_by_id(id="child1")
assert child.id == "child1"
assert child.get_child_by_id(id="grandchild1").id == "grandchild1"
assert parent.get_child_by_id(id="child2").id == "child2"
def test_get_child_by_id_no_matching_child(parent):
with pytest.raises(NoMatches):
parent.get_child_by_id(id="doesnt-exist")
def test_get_child_by_id_only_immediate_descendents(parent):
with pytest.raises(NoMatches):
parent.get_child_by_id(id="grandchild1")
def test_get_widget_by_id_no_matching_child(parent):
with pytest.raises(NoMatches):
parent.get_widget_by_id(id="i-dont-exist")
def test_get_widget_by_id_non_immediate_descendants(parent):
result = parent.get_widget_by_id("grandchild1")
assert result.id == "grandchild1"
def test_get_widget_by_id_immediate_descendants(parent):
result = parent.get_widget_by_id("child1")
assert result.id == "child1"
def test_get_widget_by_id_doesnt_return_self(parent):
with pytest.raises(NoMatches):
parent.get_widget_by_id("parent")
def test_get_widgets_app_delegated(hierarchy_app, parent):
# Check that get_child_by_id finds the parent, which is a child of the default Screen
queried_parent = hierarchy_app.get_child_by_id("parent")
assert queried_parent is parent
# Check that the grandchild (descendant of the default screen) is found
grandchild = hierarchy_app.get_widget_by_id("grandchild1")
assert grandchild.id == "grandchild1"
def test_widget_mount_ids_must_be_unique_mounting_all_in_one_go(parent):
widget1 = Widget(id="hello")
widget2 = Widget(id="hello")
with pytest.raises(MountError):
parent.mount(widget1, widget2)
def test_widget_mount_ids_must_be_unique_mounting_multiple_calls(parent):
widget1 = Widget(id="hello")
widget2 = Widget(id="hello")
parent.mount(widget1)
with pytest.raises(DuplicateIds):
parent.mount(widget2)

View File

@@ -15,18 +15,18 @@ async def test_remove_single_widget():
async def test_many_remove_all_widgets():
"""It should be possible to remove all widgets on a multi-widget screen."""
async with App().run_test() as pilot:
await pilot.app.mount(*[Static() for _ in range(1000)])
assert len(pilot.app.screen.children) == 1000
await pilot.app.mount(*[Static() for _ in range(10)])
assert len(pilot.app.screen.children) == 10
await pilot.app.query(Static).remove()
assert len(pilot.app.screen.children) == 0
async def test_many_remove_some_widgets():
"""It should be possible to remove some widgets on a multi-widget screen."""
async with App().run_test() as pilot:
await pilot.app.mount(*[Static(id=f"is-{n%2}") for n in range(1000)])
assert len(pilot.app.screen.children) == 1000
await pilot.app.query("#is-0").remove()
assert len(pilot.app.screen.children) == 500
await pilot.app.mount(*[Static(classes=f"is-{n%2}") for n in range(10)])
assert len(pilot.app.screen.children) == 10
await pilot.app.query(".is-0").remove()
assert len(pilot.app.screen.children) == 5
async def test_remove_branch():
"""It should be possible to remove a whole branch in the DOM."""