mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
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:
@@ -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")
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual import events
|
||||
from textual.containers import Container
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user