diff options
Diffstat (limited to 'packages/meshbay-hub/tests/test_transport_contracts.py')
| -rw-r--r-- | packages/meshbay-hub/tests/test_transport_contracts.py | 113 |
1 files changed, 73 insertions, 40 deletions
diff --git a/packages/meshbay-hub/tests/test_transport_contracts.py b/packages/meshbay-hub/tests/test_transport_contracts.py index 5dc3b7b..e014518 100644 --- a/packages/meshbay-hub/tests/test_transport_contracts.py +++ b/packages/meshbay-hub/tests/test_transport_contracts.py @@ -23,6 +23,12 @@ import pytest STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static" TRANSPORT = STATIC / "transport.js" APP = STATIC / "app.js" +# Chat's history paging/scroll-anchoring logic and GroupPage's own connect() +# effect moved out of app.js in the group-page refactor. +CHAT_APP = STATIC / "chat-app.js" +GROUP_PAGE = STATIC / "group-page.js" +SPLIT_FILES = [APP, GROUP_PAGE, CHAT_APP, STATIC / "files-app.js", + STATIC / "video-player.js", STATIC / "group-settings.js"] pytestmark = pytest.mark.skipif( not TRANSPORT.exists(), reason="the SPA sources are not available") @@ -38,6 +44,16 @@ def app(): return APP.read_text() +@pytest.fixture(scope="module") +def chat(): + return CHAT_APP.read_text() + + +@pytest.fixture(scope="module") +def group_page(): + return GROUP_PAGE.read_text() + + def test_chat_history_pages_backwards(transport): body = transport[transport.index("async fetchChatHistory"):] body = body[:body.index("\n }")] @@ -53,15 +69,15 @@ def test_chat_history_reports_whether_more_exists(transport): "without it the 'load older' control cannot know when to stop offering") -def test_the_browser_asks_for_the_newest_page_first(app): +def test_the_browser_asks_for_the_newest_page_first(chat): """A group opens on the newest messages, not the oldest.""" - assert "fetchChatHistory({ limit: CHAT_PAGE })" in app - assert re.search(r"const CHAT_PAGE\s*=\s*100", app) - assert re.search(r"const CHAT_OLDER_PAGE\s*=\s*50", app) + assert "fetchChatHistory({ limit: CHAT_PAGE })" in chat + assert re.search(r"const CHAT_PAGE\s*=\s*100", chat) + assert re.search(r"const CHAT_OLDER_PAGE\s*=\s*50", chat) -def test_older_pages_are_requested_with_a_cursor_not_an_offset(app): - assert "before: messages[0].id" in app, ( +def test_older_pages_are_requested_with_a_cursor_not_an_offset(chat): + assert "before: messages[0].id" in chat, ( "paging by offset repeats or skips messages when one arrives mid-scroll") @@ -88,20 +104,20 @@ def test_ping_can_time_out_sooner_than_a_transfer(transport): assert "timeoutMs" in body -def test_scroll_position_is_anchored_when_older_messages_are_prepended(app): +def test_scroll_position_is_anchored_when_older_messages_are_prepended(chat): """Everything above the viewport grows, so scrollTop alone is not enough.""" - assert "scrollHeight - list.scrollTop" in app, "the anchor is measured from the bottom" - assert "list.scrollTop = list.scrollHeight - anchorRef.current" in app - assert "useLayoutEffect" in app, ( + assert "scrollHeight - list.scrollTop" in chat, "the anchor is measured from the bottom" + assert "list.scrollTop = list.scrollHeight - anchorRef.current" in chat + assert "useLayoutEffect" in chat, ( "correcting after paint shows the jump it is meant to prevent") -def test_the_view_only_follows_new_messages_when_already_at_the_bottom(app): - assert "if (atBottomRef.current) list.scrollTop = list.scrollHeight" in app, ( +def test_the_view_only_follows_new_messages_when_already_at_the_bottom(chat): + assert "if (atBottomRef.current) list.scrollTop = list.scrollHeight" in chat, ( "scrolling unconditionally fights someone reading back through history") -def test_the_bottom_is_reached_by_scrollTop_not_a_sentinel(app): +def test_the_bottom_is_reached_by_scrollTop_not_a_sentinel(chat): """scrollIntoView on a zero-height marker stops short of the true bottom. The list has padding and a flex gap below the last bubble, so aligning an @@ -110,15 +126,15 @@ def test_the_bottom_is_reached_by_scrollTop_not_a_sentinel(app): """ # The call, not the word: a comment explaining why it is gone should not # be able to fail this. - assert ".scrollIntoView(" not in app - assert "list.scrollTo({ top: list.scrollHeight" in app, ( + assert ".scrollIntoView(" not in chat + assert "list.scrollTo({ top: list.scrollHeight" in chat, ( "jumping to the latest should also land on the real bottom") -def test_messages_are_keyed_by_id_not_index(app): +def test_messages_are_keyed_by_id_not_index(chat): """Index keys plus prepending makes Preact reuse the wrong bubbles.""" - assert "key=${m.id}" in app - assert "key=${i}" not in app.split("function ChatPanel")[1].split("\n}")[0] + assert "key=${m.id}" in chat + assert "key=${i}" not in chat.split("function ChatPanel")[1].split("\n}")[0] def test_presence_has_three_states_and_a_label_for_each(app): @@ -137,9 +153,9 @@ def _string(source: str, key: str) -> str: return source[start:end] -def test_a_refusal_from_the_node_counts_as_present(app): +def test_a_refusal_from_the_node_counts_as_present(group_page): """The node answering "no" proves it is up; only silence proves nothing.""" - assert "err.reason ? 'online' : 'offline'" in app + assert "err.reason ? 'online' : 'offline'" in group_page # ── The create-group form ───────────────────────────────────────────────────── @@ -191,7 +207,7 @@ def test_the_form_starts_on_a_combination_the_api_accepts(app): # ── Dead references in the SPA ──────────────────────────────────────────────── -def test_no_setter_survives_the_state_it_belonged_to(app): +def test_no_setter_survives_the_state_it_belonged_to(): """A removed useState leaves its setter behind, and nothing complains. `setActionsOpen` outlived `actionsOpen` when the Actions dropdown became a @@ -202,28 +218,45 @@ def test_no_setter_survives_the_state_it_belonged_to(app): A grep for the state name does not find it — `setActionsOpen` does not contain `actionsOpen`, the capital breaks the match. That is exactly how it got through. + + Checked per file rather than on one concatenated blob: the group-page + refactor split what used to be one app.js into several, and a setter + defined in one (e.g. `setAuth`, imported from hub-client.js) must not be + mistaken for covering an orphan call of the same name in another. Lifting + state to the shared shell and passing its setter down as a prop is the + same idea one level lower — `FilesPanel`'s `setEntries` is real, just + declared in group-page.js's own `useState` rather than here — so a setter + named in a component's own destructured props is treated as defined too. """ import re - declared = set(re.findall(r"const \[\s*\w+\s*,\s*(set\w+)\s*\]\s*=\s*useState", app)) - # Names brought in from another module are defined, just not here. - imported = set() - for names in re.findall(r"import\s*\{([^}]*)\}\s*from", app): - imported.update(n.strip().split(" as ")[-1].strip() for n in names.split(",")) - # A bare call only: `downloads.setMode(...)` and `view.setUint32(...)` belong - # to their object, not to this component. - called = set(re.findall(r"(?<![.\w])(set[A-Z]\w*)\s*\(", app)) - builtin = {"setTimeout", "setInterval"} - # A `setX` that is a plain function of this module is not an orphan setter: - # `setAuth` writes the session to localStorage and has no `useState` behind - # it by design. Without this the rule reports every such helper, and a rule - # that cries wolf is one someone eventually silences. - defined = set(re.findall(r"^(?:async\s+)?function\s+(set[A-Z]\w*)\s*\(", app, re.M)) - defined |= set(re.findall(r"^\s*const\s+(set[A-Z]\w*)\s*=", app, re.M)) + for path in SPLIT_FILES: + app = path.read_text() + declared = set(re.findall(r"const \[\s*\w+\s*,\s*(set\w+)\s*\]\s*=\s*useState", app)) + # Names brought in from another module are defined, just not here. + imported = set() + for names in re.findall(r"import\s*\{([^}]*)\}\s*from", app): + imported.update(n.strip().split(" as ")[-1].strip() for n in names.split(",")) + # A bare call only: `downloads.setMode(...)` and `view.setUint32(...)` + # belong to their object, not to this component. + called = set(re.findall(r"(?<![.\w])(set[A-Z]\w*)\s*\(", app)) + builtin = {"setTimeout", "setInterval"} + # A `setX` that is a plain function of this module is not an orphan + # setter: `setAuth` writes the session to localStorage and has no + # `useState` behind it by design. Without this the rule reports every + # such helper, and a rule that cries wolf is one someone eventually + # silences. + defined = set(re.findall(r"^(?:async\s+)?function\s+(set[A-Z]\w*)\s*\(", app, re.M)) + defined |= set(re.findall(r"^\s*const\s+(set[A-Z]\w*)\s*=", app, re.M)) + # A setter named in a `function Component({ ..., setX, ... })` prop + # list is handed down from wherever it is really declared. + for params in re.findall(r"^function [A-Z]\w*\(\{([^}]*)\}", app, re.M): + defined.update(re.findall(r"\b(set[A-Z]\w*)\b", params)) - orphans = sorted(called - declared - imported - builtin - defined) - assert not orphans, ( - f"setter(s) called with no useState behind them: {orphans} — " - "each one is a ReferenceError the moment that code path runs") + orphans = sorted(called - declared - imported - builtin - defined) + assert not orphans, ( + f"{path.name}: setter(s) called with no useState behind them: " + f"{orphans} — each one is a ReferenceError the moment that code " + "path runs") # ── Parallel uploads ────────────────────────────────────────────────────────── |