|
20 | 20 | from reactpy.core.component import component |
21 | 21 | from reactpy.core.events import EventHandler |
22 | 22 | from reactpy.core.hooks import use_async_effect, use_effect, use_state |
23 | | -from reactpy.core.layout import Layout, _ThreadSafeQueue |
| 23 | +from reactpy.core.layout import Layout, _ModelState, _ThreadSafeQueue |
24 | 24 | from reactpy.testing import ( |
25 | 25 | HookCatcher, |
26 | 26 | StaticEventHandler, |
@@ -1933,3 +1933,67 @@ def note_late_render(self, model_state): |
1933 | 1933 | "a render went ahead after the layout had closed, on a layout which has " |
1934 | 1934 | "already let go of what it renders with" |
1935 | 1935 | ) |
| 1936 | + |
| 1937 | + |
| 1938 | +async def test_a_closed_layout_owes_neither_an_ack_delivery_nor_a_render(): |
| 1939 | + """Nothing is promised on behalf of a layout which has closed |
| 1940 | +
|
| 1941 | + An acknowledgement reaches the client inside a render of the element it |
| 1942 | + describes, and a render is a task belonging to the layout's loop. Once the |
| 1943 | + layout has closed there is no element left, no task to create, and no client |
| 1944 | + to receive any of it - only the memory of the tables which used to hold all |
| 1945 | + that, and reaching for those would raise where nothing is left to catch it. |
| 1946 | + """ |
| 1947 | + static = StaticEventHandler() |
| 1948 | + handler = EventHandler(lambda e: None, target=static.target) |
| 1949 | + |
| 1950 | + @component |
| 1951 | + def Root(): |
| 1952 | + return html.input({"value": "text", "onChange": handler}) |
| 1953 | + |
| 1954 | + layout = Layout(Root()) |
| 1955 | + |
| 1956 | + async with layout: |
| 1957 | + await layout.render() |
| 1958 | + |
| 1959 | + layout._schedule_ack_delivery(static.target, 4) |
| 1960 | + layout._schedule_render_task("a-component-which-is-no-longer-here") |
| 1961 | + |
| 1962 | + # Neither request was allowed to reach for what teardown has released: the |
| 1963 | + # acknowledgement tables are gone and no task was created to replace them |
| 1964 | + assert getattr(layout, "_pending_ack_by_target", None) is None |
| 1965 | + assert layout._render_tasks == set() |
| 1966 | + |
| 1967 | + |
| 1968 | +def test_an_element_with_nothing_above_it_prompts_no_render(): |
| 1969 | + """The component which would carry an acknowledgement is the one above |
| 1970 | +
|
| 1971 | + Walking up from an element finds the component nearest above it, because that |
| 1972 | + is the one whose re-render rewrites the element's model. An element with |
| 1973 | + nothing left above it has no component to prompt - its model will carry the |
| 1974 | + acknowledgement the moment it is rendered again, which is all anyone can ask. |
| 1975 | + """ |
| 1976 | + parent = _ModelState( |
| 1977 | + parent=None, |
| 1978 | + index=-1, |
| 1979 | + key=None, |
| 1980 | + model=Ref({"tagName": "", "children": []}), |
| 1981 | + patch_path="", |
| 1982 | + children_by_key={}, |
| 1983 | + targets_by_event={}, |
| 1984 | + ) |
| 1985 | + element = _ModelState( |
| 1986 | + parent=parent, |
| 1987 | + index=0, |
| 1988 | + key=None, |
| 1989 | + model=Ref({"tagName": "input", "attributes": {"value": "text"}}), |
| 1990 | + patch_path="/children/0", |
| 1991 | + children_by_key={}, |
| 1992 | + targets_by_event={"onChange": "/0:onChange"}, |
| 1993 | + ) |
| 1994 | + |
| 1995 | + # Whatever was above the element is gone, so there is no component to prompt |
| 1996 | + del parent |
| 1997 | + gc.collect() |
| 1998 | + |
| 1999 | + assert Layout._owner_component_id(element) is None |
0 commit comments