aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_chat_scroll_bottom.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests/test_chat_scroll_bottom.py')
-rw-r--r--packages/meshbay-hub/tests/test_chat_scroll_bottom.py92
1 files changed, 92 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_chat_scroll_bottom.py b/packages/meshbay-hub/tests/test_chat_scroll_bottom.py
new file mode 100644
index 0000000..7b66c00
--- /dev/null
+++ b/packages/meshbay-hub/tests/test_chat_scroll_bottom.py
@@ -0,0 +1,92 @@
+"""
+Structural guards for the chat panel's scroll and fetch behaviour.
+
+These have regressed repeatedly. The tests below lock the invariants that
+prevent the known failure modes so that a future change to chat-app.js that
+breaks them fails loudly in the suite rather than silently shipping to a
+browser.
+
+Scroll (regressed five times):
+
+1. The scroll-to-bottom layout effect must depend on *both* ``messages`` and
+ ``hasMore``. The "load older" button is controlled by ``hasMore``; when it
+ appears, it pushes all messages down. If the effect ignores ``hasMore``, it
+ misses that shift and the chat stays above the last message.
+
+2. In the initial fetch callback, ``setHasMore`` must be called before
+ ``setMessages``. If the framework does not batch the two updates, messages
+ would arrive (and the scroll effect would fire) before the button is in
+ the DOM.
+
+Fetch (regressed three times):
+
+3. The history-fetch effect must depend on ``status``, not on
+ ``transportRef.current?.connected``. ChatPanel remounts on group switch
+ (keyed by groupId), and its mount effect fires *before* GroupPage's
+ cleanup releases the old transport. With a ref-based dep the effect sees
+ the old transport still connected, fetches from the wrong group, sets
+ ``loadedRef = true``, and never re-fires when the correct transport
+ connects (dep stays ``true``). Depending on the ``status`` prop avoids
+ this: GroupPage sets ``status = 'connected'`` only after the new transport
+ is fully connected and the index is fetched.
+"""
+from pathlib import Path
+
+import pytest
+
+STATIC = (Path(__file__).resolve().parents[1]
+ / "src" / "meshbay_hub" / "static")
+CHAT = STATIC / "chat-app.js"
+
+pytestmark = pytest.mark.skipif(not CHAT.exists(), reason="SPA sources not present")
+
+
+def _chat_panel_source() -> str:
+ src = CHAT.read_text()
+ start = src.index("\nfunction ChatPanel(")
+ end = src.find("\nfunction ", start + 1)
+ return src[start:end if end != -1 else len(src)]
+
+
+def test_scroll_layout_effect_depends_on_has_more():
+ """Without hasMore the 'load older' button appearing in a second render
+ is invisible to the scroll effect."""
+ src = _chat_panel_source()
+ assert "}, [messages, hasMore])" in src, (
+ "the scroll-to-bottom useLayoutEffect must depend on [messages, hasMore] "
+ "-- hasMore controls the 'load older' button, which shifts all messages "
+ "down when it appears; without it the scroll effect misses the shift")
+
+
+def test_initial_fetch_sets_has_more_before_messages():
+ """If the framework does not batch the two setState calls, calling
+ setMessages first lets the scroll effect run while the 'load older'
+ button is not yet in the DOM. Setting hasMore first means the button
+ is already present by the time messages (and the scroll) arrive."""
+ src = _chat_panel_source()
+ fetch = src[src.index("fetchChatHistory("):]
+ fetch = fetch[:fetch.index(".catch(")]
+ has_more_pos = fetch.index("setHasMore")
+ messages_pos = fetch.index("setMessages")
+ assert has_more_pos < messages_pos, (
+ "in the initial fetchChatHistory callback, setHasMore must come before "
+ "setMessages -- otherwise a non-batched render lets the scroll effect "
+ "run without the 'load older' button in the DOM")
+
+
+def test_fetch_effect_depends_on_status_not_transport_ref():
+ """The history-fetch effect must gate on the status prop, not on
+ transportRef.current?.connected. With a ref-based dep, ChatPanel's mount
+ effect (which fires before GroupPage's cleanup) sees the old transport
+ still connected, fetches from the wrong group, and never re-fires when
+ the correct transport connects."""
+ src = _chat_panel_source()
+ fetch_block = src[src.index("fetchChatHistory("):]
+ effect_end = fetch_block[:fetch_block.index("const loadOlder")]
+ assert "}, [status])" in effect_end, (
+ "the chat history fetch useEffect must depend on [status], not on "
+ "transportRef.current?.connected -- the ref-based dep races with "
+ "GroupPage's cleanup and picks up the stale transport on group switch")
+ assert "transportRef.current?.connected" not in effect_end, (
+ "transportRef.current?.connected must not appear in the fetch effect's "
+ "dependency array -- it causes a race on group switch")