aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_chat_scroll_up.py
blob: 6cafcdc0bf8174ca9a2bdc40c9c5c1afef178780 (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
93
94
95
96
97
98
99
100
101
102
"""
Reading back through the conversation must work.

The scroll-to-bottom on arrival has been patched five times, and the sixth
patch took the other side away: the panel re-pinned the list about 120 times a
second, so a wheel gesture was undone in the frame it happened in and the older
messages became unreachable. Every pin in the source is guarded by "only if the
reader is at the bottom" — the reader simply never got to stop being at the
bottom, because the `scroll` event that records it is delivered a frame after
the pin that erased it.

None of that is visible in the source, which is why this measures the real
`ChatPanel` in a browser instead of reading `chat-app.js`. `test_chat_scroll_
bottom.py` keeps the structural guards; this one keeps the behaviour.
"""
import json
import shutil
import subprocess
from pathlib import Path

import pytest

HARNESS = Path(__file__).parent / "harness" / "chat_scroll_probe.py"
STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static"

pytestmark = pytest.mark.skipif(
    shutil.which("google-chrome") is None or not (STATIC / "chat-app.js").exists(),
    reason="Chrome or the SPA sources are not available")

# A bare document fires a couple of these as it settles. Anything above this is
# the panel driving itself, which is what the loop looked like.
IDLE_RESIZE_CEILING = 20

# The harness scrolls up by six frames of 120px. Chrome's scroll anchoring
# moves the reader with the content when preview cards land above them, so the
# distance from the bottom is not expected to be exactly 720 afterwards — only
# to stay well clear of it.
SCROLLED_UP_FLOOR = 400


@pytest.fixture(scope="module")
def probe():
    run = subprocess.run(["python3", str(HARNESS)], capture_output=True, timeout=180)
    assert run.returncode == 0, run.stderr.decode()[-2000:]
    data = json.loads(run.stdout.decode())
    return data, {s["label"]: s for s in data["steps"]}


def test_arrives_on_the_newest_message(probe):
    """Opening the tab lands at the end of the conversation, after the late
    layout and the panel sizing itself."""
    _, steps = probe
    assert steps["arrived"]["fromBottom"] < 40, (
        "the chat must open on the newest message; it opened "
        f"{steps['arrived']['fromBottom']}px above it")


def test_the_panel_does_not_resize_itself(probe):
    """fit() runs on `resize` and must not produce one. When it did, it re-ran
    every frame and re-pinned the scroll with it."""
    data, _ = probe
    assert data["idle"]["viewportResizes"] <= IDLE_RESIZE_CEILING, (
        f"{data['idle']['viewportResizes']} viewport resizes on a page nobody "
        "touched -- fit() is feeding the event it listens for, and every pass "
        "re-pins the chat to the bottom")
    assert data["idle"]["documentOverflow"] <= 0, (
        "the chat tab must not leave the document taller than the window")


def test_scrolling_up_holds(probe):
    """The gesture must survive the frame it happened in."""
    _, steps = probe
    assert steps["scrolled up"]["fromBottom"] >= SCROLLED_UP_FLOOR, (
        "scrolling up was undone: the list came back to "
        f"{steps['scrolled up']['fromBottom']}px from the bottom")
    assert steps["scrolled up"]["jumpButton"], (
        "the panel never noticed the reader leave the bottom -- the pin beat "
        "the scroll event, so atBottom stayed true and no jump button appeared")


def test_late_content_does_not_drag_the_reader_down(probe):
    """Link previews and thumbnails arrive over the following seconds and grow
    the list. That is what the arrival pin is for, and it must be over by
    now."""
    _, steps = probe
    assert steps["previews landed"]["fromBottom"] >= SCROLLED_UP_FLOOR, (
        "preview cards landing pulled the reader back to the bottom")


def test_a_new_message_does_not_yank_the_reader_down(probe):
    """Somebody writing while you read back marks the spot; it does not move
    you."""
    _, steps = probe
    assert steps["message arrived"]["fromBottom"] >= SCROLLED_UP_FLOOR, (
        "an incoming message pulled the reader away from what they were reading")


def test_a_resize_does_not_yank_the_reader_down(probe):
    """A window resize, a rotation, or a phone's URL bar collapsing."""
    _, steps = probe
    assert steps["window resized"]["fromBottom"] >= SCROLLED_UP_FLOOR, (
        "a resize pulled the reader back to the bottom")