I can persist tabmanager state
This commit is contained in:
14
src/app.py
14
src/app.py
@@ -5,7 +5,8 @@ from fasthtml.components import *
|
|||||||
|
|
||||||
from myfasthtml.controls.Layout import Layout
|
from myfasthtml.controls.Layout import Layout
|
||||||
from myfasthtml.controls.TabsManager import TabsManager
|
from myfasthtml.controls.TabsManager import TabsManager
|
||||||
from myfasthtml.controls.helpers import Ids
|
from myfasthtml.controls.helpers import Ids, mk
|
||||||
|
from myfasthtml.core.commands import Command
|
||||||
from myfasthtml.core.instances import InstancesManager
|
from myfasthtml.core.instances import InstancesManager
|
||||||
from myfasthtml.myfastapp import create_app
|
from myfasthtml.myfastapp import create_app
|
||||||
|
|
||||||
@@ -30,11 +31,14 @@ def index(session):
|
|||||||
for i in range(1000):
|
for i in range(1000):
|
||||||
layout.left_drawer.append(Div(f"Left Drawer Item {i}"))
|
layout.left_drawer.append(Div(f"Left Drawer Item {i}"))
|
||||||
|
|
||||||
tabs_manager = InstancesManager.get(session, Ids.TabsManager, TabsManager)
|
tabs_manager = TabsManager(session, _id="main")
|
||||||
tabs_manager.add_tab("Users", Div("Content 1"))
|
btn = mk.button("Add Tab",
|
||||||
tabs_manager.add_tab("Users", Div("Content 2"))
|
command=Command("AddTab",
|
||||||
tabs_manager.add_tab("Users", Div("Content 3"))
|
"Add a new tab",
|
||||||
|
tabs_manager.on_new_tab, "Tabs", Div("Content")).
|
||||||
|
htmx(target=f"#{tabs_manager.get_id()}"))
|
||||||
layout.set_main(tabs_manager)
|
layout.set_main(tabs_manager)
|
||||||
|
layout.set_footer(btn)
|
||||||
return layout
|
return layout
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ class BaseCommands:
|
|||||||
def __init__(self, owner):
|
def __init__(self, owner):
|
||||||
self._owner = owner
|
self._owner = owner
|
||||||
self._id = owner.get_id()
|
self._id = owner.get_id()
|
||||||
|
self._prefix = owner.get_prefix()
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from fasthtml.common import Div, Button, Span
|
from fasthtml.common import Div, Button, Span
|
||||||
|
|
||||||
from myfasthtml.controls.BaseCommands import BaseCommands
|
from myfasthtml.controls.BaseCommands import BaseCommands
|
||||||
from myfasthtml.controls.helpers import Ids
|
from myfasthtml.controls.helpers import Ids, mk
|
||||||
|
from myfasthtml.core.commands import Command
|
||||||
from myfasthtml.core.dbmanager import DbObject
|
from myfasthtml.core.dbmanager import DbObject
|
||||||
from myfasthtml.core.instances import MultipleInstance, BaseInstance
|
from myfasthtml.core.instances import MultipleInstance, BaseInstance
|
||||||
from myfasthtml.icons.fluent_p3 import dismiss_circle16_regular
|
from myfasthtml.icons.fluent_p3 import dismiss_circle16_regular
|
||||||
@@ -35,18 +35,25 @@ class TabsManagerState(DbObject):
|
|||||||
|
|
||||||
|
|
||||||
class Commands(BaseCommands):
|
class Commands(BaseCommands):
|
||||||
pass
|
def show_tab(self, tab_id):
|
||||||
|
return Command(f"{self._prefix}SowTab",
|
||||||
|
"Activate or show a specific tab",
|
||||||
|
self._owner.show_tab, tab_id).htmx(target=f"#{self._id}", swap="outerHTML")
|
||||||
|
|
||||||
|
|
||||||
class TabsManager(MultipleInstance):
|
class TabsManager(MultipleInstance):
|
||||||
def __init__(self, session):
|
def __init__(self, session, _id=None):
|
||||||
super().__init__(session, Ids.TabsManager)
|
super().__init__(session, Ids.TabsManager, _id=_id)
|
||||||
self._state = TabsManagerState(self)
|
self._state = TabsManagerState(self)
|
||||||
self._commands = Commands(self)
|
self.commands = Commands(self)
|
||||||
|
|
||||||
def get_state(self):
|
def get_state(self):
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
|
def on_new_tab(self, label: str, component: Any):
|
||||||
|
self.add_tab(label, component)
|
||||||
|
return self
|
||||||
|
|
||||||
def add_tab(self, label: str, component: Any, activate: bool = True) -> str:
|
def add_tab(self, label: str, component: Any, activate: bool = True) -> str:
|
||||||
"""
|
"""
|
||||||
Add a new tab or update an existing one with the same component type, ID and label.
|
Add a new tab or update an existing one with the same component type, ID and label.
|
||||||
@@ -108,6 +115,13 @@ class TabsManager(MultipleInstance):
|
|||||||
|
|
||||||
return tab_id
|
return tab_id
|
||||||
|
|
||||||
|
def show_tab(self, tab_id):
|
||||||
|
if tab_id not in self._state.tabs:
|
||||||
|
return None
|
||||||
|
|
||||||
|
self._state.active_tab = tab_id
|
||||||
|
return self
|
||||||
|
|
||||||
def _mk_tab_button(self, tab_id: str, tab_data: dict):
|
def _mk_tab_button(self, tab_id: str, tab_data: dict):
|
||||||
"""
|
"""
|
||||||
Create a single tab button with its label and close button.
|
Create a single tab button with its label and close button.
|
||||||
@@ -122,7 +136,7 @@ class TabsManager(MultipleInstance):
|
|||||||
is_active = tab_id == self._state.active_tab
|
is_active = tab_id == self._state.active_tab
|
||||||
|
|
||||||
return Button(
|
return Button(
|
||||||
Span(tab_data.get("label", "Untitled"), cls="mf-tab-label"),
|
mk.mk(Span(tab_data.get("label", "Untitled"), cls="mf-tab-label"), command=self.commands.show_tab(tab_id)),
|
||||||
Span(dismiss_circle16_regular, cls="mf-tab-close-btn"),
|
Span(dismiss_circle16_regular, cls="mf-tab-close-btn"),
|
||||||
cls=f"mf-tab-button {'mf-tab-active' if is_active else ''}",
|
cls=f"mf-tab-button {'mf-tab-active' if is_active else ''}",
|
||||||
data_tab_id=tab_id,
|
data_tab_id=tab_id,
|
||||||
|
|||||||
@@ -42,14 +42,7 @@ class DbObject:
|
|||||||
self._name = name or self.__class__.__name__
|
self._name = name or self.__class__.__name__
|
||||||
self._db_manager = db_manager or InstancesManager.get(self._session, Ids.DbManager, DbManager)
|
self._db_manager = db_manager or InstancesManager.get(self._session, Ids.DbManager, DbManager)
|
||||||
|
|
||||||
# init is possible
|
self._finalize_initialization()
|
||||||
if self._db_manager.exists_entry(self._name):
|
|
||||||
props = self._db_manager.load(self._name)
|
|
||||||
for k, v in props.items():
|
|
||||||
if hasattr(self, k):
|
|
||||||
setattr(self, k, v)
|
|
||||||
else:
|
|
||||||
self._save_self()
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def initializing(self):
|
def initializing(self):
|
||||||
@@ -58,8 +51,8 @@ class DbObject:
|
|||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
|
self._finalize_initialization()
|
||||||
self._initializing = old_state
|
self._initializing = old_state
|
||||||
self._save_self()
|
|
||||||
|
|
||||||
def __setattr__(self, name: str, value: str):
|
def __setattr__(self, name: str, value: str):
|
||||||
if name.startswith("_") or getattr(self, "_initializing", False):
|
if name.startswith("_") or getattr(self, "_initializing", False):
|
||||||
@@ -73,6 +66,13 @@ class DbObject:
|
|||||||
super().__setattr__(name, value)
|
super().__setattr__(name, value)
|
||||||
self._save_self()
|
self._save_self()
|
||||||
|
|
||||||
|
def _finalize_initialization(self):
|
||||||
|
if self._db_manager.exists_entry(self._name):
|
||||||
|
props = self._db_manager.load(self._name)
|
||||||
|
self.update(props)
|
||||||
|
else:
|
||||||
|
self._save_self()
|
||||||
|
|
||||||
def _save_self(self):
|
def _save_self(self):
|
||||||
props = {k: getattr(self, k) for k, v in self._get_properties().items() if not k.startswith("_")}
|
props = {k: getattr(self, k) for k, v in self._get_properties().items() if not k.startswith("_")}
|
||||||
if props:
|
if props:
|
||||||
@@ -103,11 +103,14 @@ class DbObject:
|
|||||||
|
|
||||||
properties |= kwargs
|
properties |= kwargs
|
||||||
|
|
||||||
with self.initializing():
|
# save the new state
|
||||||
for k, v in properties.items():
|
old_state = getattr(self, "_initializing", False)
|
||||||
if hasattr(self, k) and k not in DbObject._forbidden_attrs: # internal variables cannot be updated
|
self._initializing = True
|
||||||
setattr(self, k, v)
|
for k, v in properties.items():
|
||||||
|
if hasattr(self, k) and k not in DbObject._forbidden_attrs: # internal variables cannot be updated
|
||||||
|
setattr(self, k, v)
|
||||||
self._save_self()
|
self._save_self()
|
||||||
|
self._initializing = old_state
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
as_dict = self._get_properties().copy()
|
as_dict = self._get_properties().copy()
|
||||||
|
|||||||
@@ -17,9 +17,10 @@ class BaseInstance:
|
|||||||
Base class for all instances (manageable by InstancesManager)
|
Base class for all instances (manageable by InstancesManager)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, session: dict, _id: str, auto_register: bool = True):
|
def __init__(self, session: dict, prefix: str, _id: str, auto_register: bool = True):
|
||||||
self._session = session
|
self._session = session
|
||||||
self._id = _id
|
self._id = _id
|
||||||
|
self._prefix = prefix
|
||||||
if auto_register:
|
if auto_register:
|
||||||
InstancesManager.register(session, self)
|
InstancesManager.register(session, self)
|
||||||
|
|
||||||
@@ -29,6 +30,9 @@ class BaseInstance:
|
|||||||
def get_session(self):
|
def get_session(self):
|
||||||
return self._session
|
return self._session
|
||||||
|
|
||||||
|
def get_prefix(self):
|
||||||
|
return self._prefix
|
||||||
|
|
||||||
|
|
||||||
class SingleInstance(BaseInstance):
|
class SingleInstance(BaseInstance):
|
||||||
"""
|
"""
|
||||||
@@ -36,8 +40,7 @@ class SingleInstance(BaseInstance):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, session: dict, prefix: str, auto_register: bool = True):
|
def __init__(self, session: dict, prefix: str, auto_register: bool = True):
|
||||||
super().__init__(session, prefix, auto_register)
|
super().__init__(session, prefix, prefix, auto_register)
|
||||||
self._instance = None
|
|
||||||
|
|
||||||
|
|
||||||
class UniqueInstance(BaseInstance):
|
class UniqueInstance(BaseInstance):
|
||||||
@@ -47,8 +50,8 @@ class UniqueInstance(BaseInstance):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, session: dict, prefix: str, auto_register: bool = True):
|
def __init__(self, session: dict, prefix: str, auto_register: bool = True):
|
||||||
super().__init__(session, prefix, auto_register)
|
super().__init__(session, prefix, prefix, auto_register)
|
||||||
self._instance = None
|
self._prefix = prefix
|
||||||
|
|
||||||
|
|
||||||
class MultipleInstance(BaseInstance):
|
class MultipleInstance(BaseInstance):
|
||||||
@@ -56,9 +59,9 @@ class MultipleInstance(BaseInstance):
|
|||||||
Base class for instances that can have multiple instances at a time.
|
Base class for instances that can have multiple instances at a time.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, session: dict, prefix: str, auto_register: bool = True):
|
def __init__(self, session: dict, prefix: str, auto_register: bool = True, _id=None):
|
||||||
super().__init__(session, f"{prefix}-{str(uuid.uuid4())}", auto_register)
|
super().__init__(session, prefix, f"{prefix}-{_id or str(uuid.uuid4())}", auto_register)
|
||||||
self._instance = None
|
self._prefix = prefix
|
||||||
|
|
||||||
|
|
||||||
class InstancesManager:
|
class InstancesManager:
|
||||||
|
|||||||
@@ -90,6 +90,14 @@ class Empty(ChildrenPredicate):
|
|||||||
return len(actual.children) == 0 and len(actual.attrs) == 0
|
return len(actual.children) == 0 and len(actual.attrs) == 0
|
||||||
|
|
||||||
|
|
||||||
|
class NoChildren(ChildrenPredicate):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(None)
|
||||||
|
|
||||||
|
def validate(self, actual):
|
||||||
|
return len(actual.children) == 0
|
||||||
|
|
||||||
|
|
||||||
class AttributeForbidden(ChildrenPredicate):
|
class AttributeForbidden(ChildrenPredicate):
|
||||||
"""
|
"""
|
||||||
To validate that an attribute is not present in an element.
|
To validate that an attribute is not present in an element.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from fasthtml.components import *
|
|||||||
|
|
||||||
from myfasthtml.controls.TabsManager import TabsManager
|
from myfasthtml.controls.TabsManager import TabsManager
|
||||||
from myfasthtml.core.instances import InstancesManager
|
from myfasthtml.core.instances import InstancesManager
|
||||||
|
from myfasthtml.test.matcher import matches, NoChildren
|
||||||
from .conftest import session
|
from .conftest import session
|
||||||
|
|
||||||
|
|
||||||
@@ -13,18 +14,75 @@ def tabs_manager(session):
|
|||||||
InstancesManager.reset()
|
InstancesManager.reset()
|
||||||
|
|
||||||
|
|
||||||
def test_tabs_manager_is_registered(session, tabs_manager):
|
class TestTabsManagerBehaviour:
|
||||||
from_instance_manager = InstancesManager.get(session, tabs_manager.get_id())
|
def test_tabs_manager_is_registered(self, session, tabs_manager):
|
||||||
assert from_instance_manager == tabs_manager
|
from_instance_manager = InstancesManager.get(session, tabs_manager.get_id())
|
||||||
|
assert from_instance_manager == tabs_manager
|
||||||
|
|
||||||
|
def test_i_can_add_tab(self, tabs_manager):
|
||||||
|
tab_id = tabs_manager.add_tab("Users", Div("Content 1"))
|
||||||
|
|
||||||
|
assert tab_id is not None
|
||||||
|
assert tab_id in tabs_manager.get_state().tabs
|
||||||
|
assert tabs_manager.get_state().tabs[tab_id]["label"] == "Users"
|
||||||
|
assert tabs_manager.get_state().tabs[tab_id]["component_type"] is None # Div is not BaseInstance
|
||||||
|
assert tabs_manager.get_state().tabs[tab_id]["component_id"] is None # Div is not BaseInstance
|
||||||
|
assert tabs_manager.get_state().tabs_order == [tab_id]
|
||||||
|
assert tabs_manager.get_state().active_tab == tab_id
|
||||||
|
|
||||||
|
def test_i_can_add_multiple_tabs(self, tabs_manager):
|
||||||
|
tab_id1 = tabs_manager.add_tab("Users", Div("Content 1"))
|
||||||
|
tab_id2 = tabs_manager.add_tab("User2", Div("Content 2"))
|
||||||
|
|
||||||
|
assert len(tabs_manager.get_state().tabs) == 2
|
||||||
|
assert tabs_manager.get_state().tabs_order == [tab_id1, tab_id2]
|
||||||
|
assert tabs_manager.get_state().active_tab == tab_id2
|
||||||
|
|
||||||
|
|
||||||
def test_i_can_add_tab(tabs_manager):
|
class TestTabsManagerRender:
|
||||||
tab_id = tabs_manager.add_tab("Users", Div("Content 1"))
|
def test_i_can_render_when_no_tabs(self, tabs_manager):
|
||||||
|
res = tabs_manager.render()
|
||||||
|
|
||||||
assert tab_id is not None
|
expected = Div(
|
||||||
assert tab_id in tabs_manager.get_state().tabs
|
Div(NoChildren(), id=f"{tabs_manager.get_id()}-header"),
|
||||||
assert tabs_manager.get_state().tabs[tab_id]["label"] == "Users"
|
Div(id=f"{tabs_manager.get_id()}-content"),
|
||||||
assert tabs_manager.get_state().tabs[tab_id]["component_type"] is None # Div is not BaseInstance
|
id=tabs_manager.get_id(),
|
||||||
assert tabs_manager.get_state().tabs[tab_id]["component_id"] is None # Div is not BaseInstance
|
)
|
||||||
assert tabs_manager.get_state().tabs_order == [tab_id]
|
|
||||||
assert tabs_manager.get_state().active_tab == tab_id
|
assert matches(res, expected)
|
||||||
|
|
||||||
|
def test_i_can_render_when_one_tab(self, tabs_manager):
|
||||||
|
tabs_manager.add_tab("Users", Div("Content 1"))
|
||||||
|
res = tabs_manager.render()
|
||||||
|
|
||||||
|
expected = Div(
|
||||||
|
Div(
|
||||||
|
Button(),
|
||||||
|
id=f"{tabs_manager.get_id()}-header"
|
||||||
|
),
|
||||||
|
Div(
|
||||||
|
Div("Content 1")
|
||||||
|
),
|
||||||
|
id=tabs_manager.get_id(),
|
||||||
|
)
|
||||||
|
assert matches(res, expected)
|
||||||
|
|
||||||
|
def test_i_can_render_when_multiple_tabs(self, tabs_manager):
|
||||||
|
tabs_manager.add_tab("Users1", Div("Content 1"))
|
||||||
|
tabs_manager.add_tab("Users2", Div("Content 2"))
|
||||||
|
tabs_manager.add_tab("Users3", Div("Content 3"))
|
||||||
|
res = tabs_manager.render()
|
||||||
|
|
||||||
|
expected = Div(
|
||||||
|
Div(
|
||||||
|
Button(),
|
||||||
|
Button(),
|
||||||
|
Button(),
|
||||||
|
id=f"{tabs_manager.get_id()}-header"
|
||||||
|
),
|
||||||
|
Div(
|
||||||
|
Div("Content 3")
|
||||||
|
),
|
||||||
|
id=tabs_manager.get_id(),
|
||||||
|
)
|
||||||
|
assert matches(res, expected)
|
||||||
|
|||||||
@@ -70,7 +70,25 @@ def test_i_can_init_from_dataclass(session, db_manager):
|
|||||||
assert len(history) == 1
|
assert len(history) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_i_can_init_from_db(session, db_manager):
|
def test_i_can_init_from_db_with(session, db_manager):
|
||||||
|
class DummyObject(DbObject):
|
||||||
|
def __init__(self, sess: dict):
|
||||||
|
super().__init__(sess, "DummyObject", db_manager)
|
||||||
|
|
||||||
|
with self.initializing():
|
||||||
|
self.value: str = "hello"
|
||||||
|
self.number: int = 42
|
||||||
|
|
||||||
|
# insert other values in db
|
||||||
|
db_manager.save("DummyObject", {"value": "other_value", "number": 34})
|
||||||
|
|
||||||
|
dummy = DummyObject(session)
|
||||||
|
|
||||||
|
assert dummy.value == "other_value"
|
||||||
|
assert dummy.number == 34
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_init_from_db_with_dataclass(session, db_manager):
|
||||||
@dataclass
|
@dataclass
|
||||||
class DummyObject(DbObject):
|
class DummyObject(DbObject):
|
||||||
def __init__(self, sess: dict):
|
def __init__(self, sess: dict):
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from fastcore.basics import NotStr
|
|||||||
from fasthtml.components import *
|
from fasthtml.components import *
|
||||||
|
|
||||||
from myfasthtml.test.matcher import matches, StartsWith, Contains, DoesNotContain, Empty, DoNotCheck, ErrorOutput, \
|
from myfasthtml.test.matcher import matches, StartsWith, Contains, DoesNotContain, Empty, DoNotCheck, ErrorOutput, \
|
||||||
ErrorComparisonOutput, AttributeForbidden, AnyValue
|
ErrorComparisonOutput, AttributeForbidden, AnyValue, NoChildren
|
||||||
from myfasthtml.test.testclient import MyFT
|
from myfasthtml.test.testclient import MyFT
|
||||||
|
|
||||||
|
|
||||||
@@ -24,6 +24,8 @@ from myfasthtml.test.testclient import MyFT
|
|||||||
([Div(), Span()], DoNotCheck()),
|
([Div(), Span()], DoNotCheck()),
|
||||||
(NotStr("123456"), NotStr("123")), # for NotStr, only the beginning is checked
|
(NotStr("123456"), NotStr("123")), # for NotStr, only the beginning is checked
|
||||||
(Div(), Div(Empty())),
|
(Div(), Div(Empty())),
|
||||||
|
(Div(), Div(NoChildren())),
|
||||||
|
(Div(attr1="value"), Div(NoChildren())),
|
||||||
(Div(attr1="value1"), Div(AttributeForbidden("attr2"))),
|
(Div(attr1="value1"), Div(AttributeForbidden("attr2"))),
|
||||||
(Div(123), Div(123)),
|
(Div(123), Div(123)),
|
||||||
(Div(Span(123)), Div(Span(123))),
|
(Div(Span(123)), Div(Span(123))),
|
||||||
@@ -54,6 +56,7 @@ def test_i_can_match(actual, expected):
|
|||||||
(Div(), Div(attr1=AnyValue()), "'attr1' is not found in Actual"),
|
(Div(), Div(attr1=AnyValue()), "'attr1' is not found in Actual"),
|
||||||
(NotStr("456"), NotStr("123"), "Notstr values are different"),
|
(NotStr("456"), NotStr("123"), "Notstr values are different"),
|
||||||
(Div(attr="value"), Div(Empty()), "The condition 'Empty()' is not satisfied"),
|
(Div(attr="value"), Div(Empty()), "The condition 'Empty()' is not satisfied"),
|
||||||
|
(Div(Span()), Div(NoChildren()), "The condition 'NoChildren()' is not satisfied"),
|
||||||
(Div(120), Div(Empty()), "The condition 'Empty()' is not satisfied"),
|
(Div(120), Div(Empty()), "The condition 'Empty()' is not satisfied"),
|
||||||
(Div(Span()), Div(Empty()), "The condition 'Empty()' is not satisfied"),
|
(Div(Span()), Div(Empty()), "The condition 'Empty()' is not satisfied"),
|
||||||
(Div(), Div(Span()), "Actual is lesser than expected"),
|
(Div(), Div(Span()), "Actual is lesser than expected"),
|
||||||
@@ -203,6 +206,7 @@ def test_i_can_output_error_child_element():
|
|||||||
')',
|
')',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_i_can_output_error_child_element_text():
|
def test_i_can_output_error_child_element_text():
|
||||||
"""I can display error when the children is not a FT"""
|
"""I can display error when the children is not a FT"""
|
||||||
elt = Div("Hello world", Div(id="child_1"), Div(id="child_2"), attr1="value1")
|
elt = Div("Hello world", Div(id="child_1"), Div(id="child_2"), attr1="value1")
|
||||||
@@ -217,6 +221,7 @@ def test_i_can_output_error_child_element_text():
|
|||||||
')',
|
')',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_i_can_output_error_child_element_indicating_sub_children():
|
def test_i_can_output_error_child_element_indicating_sub_children():
|
||||||
elt = Div(P(id="p_id"), Div(Div(id="child_2"), id="child_1"), attr1="value1")
|
elt = Div(P(id="p_id"), Div(Div(id="child_2"), id="child_1"), attr1="value1")
|
||||||
expected = elt
|
expected = elt
|
||||||
|
|||||||
Reference in New Issue
Block a user