aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_chat_scroll_up.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests/test_chat_scroll_up.py')
-rw-r--r--packages/meshbay-hub/tests/test_chat_scroll_up.py102
1 files changed, 102 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_chat_scroll_up.py b/packages/meshbay-hub/tests/test_chat_scroll_up.py
new file mode 100644
index 0000000..6cafcdc
--- /dev/null
+++ b/packages/meshbay-hub/tests/test_chat_scroll_up.py
@@ -0,0 +1,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")