summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_chat_scroll_bottom.py
blob: 7b66c002ea817a14c50ada5ec894916c5da2c87d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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")