diff options
Diffstat (limited to 'packages/meshbay-hub/tests')
14 files changed, 182 insertions, 93 deletions
diff --git a/packages/meshbay-hub/tests/harness/scroll_probe.py b/packages/meshbay-hub/tests/harness/scroll_probe.py index 73ff0f1..46d8357 100644 --- a/packages/meshbay-hub/tests/harness/scroll_probe.py +++ b/packages/meshbay-hub/tests/harness/scroll_probe.py @@ -28,7 +28,9 @@ import time from pathlib import Path STATIC = Path(__file__).resolve().parents[2] / "src" / "meshbay_hub" / "static" -APP = STATIC / "app.js" +# fit() is ChatPanel's own viewport-sizing logic, moved to chat-app.js in the +# group-page refactor. +APP = STATIC / "chat-app.js" PORT = 8736 FRAG = Path(sys.argv[1]).read_text() HEIGHTS = ([int(h) for h in sys.argv[2].split(",")] diff --git a/packages/meshbay-hub/tests/harness/session_harness.mjs b/packages/meshbay-hub/tests/harness/session_harness.mjs index 3be862e..0334813 100644 --- a/packages/meshbay-hub/tests/harness/session_harness.mjs +++ b/packages/meshbay-hub/tests/harness/session_harness.mjs @@ -24,8 +24,8 @@ const between = (from, to) => { if (j < 0) throw new Error(`not found: ${to}`); return app.slice(i, j); }; -const sessionBlock = between('let _auth = loadAuth();', '\n// ── Theme'); -const hubFetchFn = between('async function hubFetch(', '\n// ── Router'); +const sessionBlock = between('let _auth = loadAuth();', '\n// ── Hub API'); +const hubFetchFn = between('async function hubFetch(', '\nexport {'); // ── The world ──────────────────────────────────────────────────────────────── const store = new Map(); diff --git a/packages/meshbay-hub/tests/test_downloads.py b/packages/meshbay-hub/tests/test_downloads.py index 85bf488..80c3b05 100644 --- a/packages/meshbay-hub/tests/test_downloads.py +++ b/packages/meshbay-hub/tests/test_downloads.py @@ -145,7 +145,9 @@ def test_a_length_is_only_promised_when_it_is_known(tmp_path): src = SW.read_text() assert "if (entry.size > 0)" in src - app = (STATIC / "app.js").read_text() + # The zip-directory download is Files' own, moved to files-app.js in the + # group-page refactor. + app = (STATIC / "files-app.js").read_text() zip_call = app[app.index("const target = await _openDownloadTarget(suggested"):] zip_call = zip_call[:zip_call.index(");") + 2] assert zip_call.rstrip().endswith(", 0);"), ( diff --git a/packages/meshbay-hub/tests/test_hook_ordering.py b/packages/meshbay-hub/tests/test_hook_ordering.py index cd9a11e..dac1357 100644 --- a/packages/meshbay-hub/tests/test_hook_ordering.py +++ b/packages/meshbay-hub/tests/test_hook_ordering.py @@ -26,6 +26,17 @@ import pytest STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static" APP = STATIC / "app.js" +# One monolithic app.js used to hold every component; the group-page refactor +# split it into one file per "application" (chat-app.js, files-app.js, +# video-player.js, group-settings.js) plus the group shell (group-page.js). +# A future Videos/Music/Photos app lands in its own file the same way — add it +# here so this test keeps seeing it, since `_all_components` below only walks +# the files named in this list. +STATIC_FILES = [ + "app.js", "group-page.js", "chat-app.js", "files-app.js", + "video-player.js", "group-settings.js", +] + pytestmark = pytest.mark.skipif(not APP.exists(), reason="SPA sources unavailable") # `const NAME = useCallback(` / `useMemo(` — the declarations that both define a @@ -40,18 +51,28 @@ def app(): return APP.read_text() -def _components(app: str): - """Each top-level component, with the offset it starts at.""" - for m in re.finditer(r"^function ([A-Z]\w*)\(", app, re.M): +def _components(src: str): + """Each top-level component in one file, with the offset it starts at.""" + for m in re.finditer(r"^function ([A-Z]\w*)\(", src, re.M): start = m.start() - nxt = app.find("\nfunction ", start + 1) - yield m.group(1), app[start:nxt if nxt > 0 else len(app)] + nxt = src.find("\nfunction ", start + 1) + yield m.group(1), src[start:nxt if nxt > 0 else len(src)] + + +def _all_components(): + """Every top-level component across every static file that can hold one.""" + for name in STATIC_FILES: + path = STATIC / name + if not path.exists(): + continue + for cname, body in _components(path.read_text()): + yield f"{name}:{cname}", body -def test_no_hook_depends_on_something_declared_below_it(app): - """The whole file, not just the player that was broken by it.""" +def test_no_hook_depends_on_something_declared_below_it(): + """Every static file that can hold a component, not just app.js.""" problems = [] - for name, body in _components(app): + for name, body in _all_components(): # Where each hook binding becomes usable. declared_at = {m.group(1): m.start() for m in DECL.finditer(body)} for deps in DEPS.finditer(body): @@ -68,13 +89,13 @@ def test_no_hook_depends_on_something_declared_below_it(app): + "\n ".join(problems)) -def test_the_check_would_notice(app): +def test_the_check_would_notice(): """A test that cannot fail proves nothing — so make it fail on purpose. Swaps two declarations in the real file and confirms the rule fires. If this stops working the rule above has quietly become decoration. """ - body = next(b for n, b in _components(app) if n == "VideoPlayer") + body = next(b for n, b in _all_components() if n == "video-player.js:VideoPlayer") decls = list(DECL.finditer(body)) assert len(decls) >= 2, "VideoPlayer has too few hooks to test the check" diff --git a/packages/meshbay-hub/tests/test_layout_responsive.py b/packages/meshbay-hub/tests/test_layout_responsive.py index 0e2444c..11a6d05 100644 --- a/packages/meshbay-hub/tests/test_layout_responsive.py +++ b/packages/meshbay-hub/tests/test_layout_responsive.py @@ -112,7 +112,9 @@ def test_the_toolbar_still_fits_a_360px_screen(css): # ── The chat panel's height ─────────────────────────────────────────────────── -APP = STATIC / "app.js" +# fit() and its constants are ChatPanel's own viewport-sizing logic, moved to +# chat-app.js in the group-page refactor. +APP = STATIC / "chat-app.js" @pytest.fixture(scope="module") diff --git a/packages/meshbay-hub/tests/test_resume_position.py b/packages/meshbay-hub/tests/test_resume_position.py index ef239c5..07ac23a 100644 --- a/packages/meshbay-hub/tests/test_resume_position.py +++ b/packages/meshbay-hub/tests/test_resume_position.py @@ -21,7 +21,7 @@ from pathlib import Path import pytest STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static" -APP = STATIC / "app.js" +APP = STATIC / "video-player.js" pytestmark = pytest.mark.skipif( shutil.which("node") is None or not APP.exists(), diff --git a/packages/meshbay-hub/tests/test_session_renewal.py b/packages/meshbay-hub/tests/test_session_renewal.py index 6c47f7a..a839f1e 100644 --- a/packages/meshbay-hub/tests/test_session_renewal.py +++ b/packages/meshbay-hub/tests/test_session_renewal.py @@ -34,7 +34,12 @@ from pathlib import Path import pytest STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static" -APP = STATIC / "app.js" +APP = STATIC / "hub-client.js" +# The session/token machinery lives in hub-client.js (APP, above); the group +# shell's own WebRTC-connect effect that consumes it is in group-page.js; the +# periodic re-check that catches a backgrounded tab is App() in app.js. +GROUP_PAGE = STATIC / "group-page.js" +APP_JS = STATIC / "app.js" HARNESS = Path(__file__).parent / "harness" / "session_harness.mjs" pytestmark = pytest.mark.skipif( @@ -182,7 +187,7 @@ def test_renewal_happens_before_expiry_not_after(): assert margin >= 300, ( f"{margin} s of margin against a one-hour token is thin: a backgrounded " "tab has its timers throttled and may not check for minutes") - assert "visibilitychange" in src, ( + assert "visibilitychange" in APP_JS.read_text(), ( "nothing re-checks when the tab comes back, which is exactly when the " "token is most likely to have aged out unnoticed") @@ -203,7 +208,7 @@ def test_renewing_does_not_tear_down_the_webrtc_connection(): Signing in or out must still re-run it, so the dependency is whether there is a token, not which one. """ - src = APP.read_text() + src = GROUP_PAGE.read_text() i = src.index("means tearing down the WebRTC connection") deps = src[i:src.index(");", i)] assert "Boolean(token)" in deps, ( @@ -218,7 +223,7 @@ def test_the_connection_signs_its_offer_with_a_live_token(): It signs the offer relayed through the hub, where an expired one is a 401 and no connection at all. """ - src = APP.read_text() + src = GROUP_PAGE.read_text() connect = src[src.index("const connect = async () => {"):] connect = connect[:connect.index("\n };")] assert "await ensureFreshToken()" in connect, ( diff --git a/packages/meshbay-hub/tests/test_spa_ordering.py b/packages/meshbay-hub/tests/test_spa_ordering.py index dfc92f9..d00b9a0 100644 --- a/packages/meshbay-hub/tests/test_spa_ordering.py +++ b/packages/meshbay-hub/tests/test_spa_ordering.py @@ -113,14 +113,26 @@ def test_the_ack_still_verifies_the_announced_node_key(): # Users tab referencing `doInvite`, `members` and `adminId` — none of which are # defined there. +# The group-page refactor split what used to be one app.js into one file per +# "application" (chat-app.js, files-app.js, video-player.js) plus +# group-settings.js and the group shell itself, group-page.js. AdminPage and +# App() stayed in app.js. `_component` below is told which file to read a +# given top-level component from. APP = STATIC / "app.js" +COMPONENT_FILES = { + "GroupSettingsPanel": STATIC / "group-settings.js", + "GroupPage": STATIC / "group-page.js", + "ChatPanel": STATIC / "chat-app.js", + "FilesPanel": STATIC / "files-app.js", + "VideoPlayer": STATIC / "video-player.js", +} def _component(name: str) -> str: """The source of one top-level `function Name(...)`, up to the next one.""" - source = APP.read_text() + source = COMPONENT_FILES.get(name, APP).read_text() start = source.find(f"\nfunction {name}(") - assert start != -1, f"{name} is gone from app.js — update this test" + assert start != -1, f"{name} is gone — update this test" end = source.find("\nfunction ", start + 1) return source[start:end if end != -1 else len(source)] @@ -220,7 +232,7 @@ def test_no_caller_waits_for_one_chunk_at_a_time(): # two ends of that, since neither shows up in any Python test. def test_leaving_a_group_hands_the_transport_over_rather_than_closing_it(): - app = APP.read_text() + app = _component("GroupPage") cleanup = app[app.index(" return () => {\n cancelled = true;"):] cleanup = cleanup[:cleanup.index("\n }, [groupId")] assert "releaseWhenIdle" in cleanup, ( @@ -252,7 +264,7 @@ def test_a_multi_file_download_waits_for_each_picker(): meant the first opened a dialog and the rest were rejected — two files selected, one file downloaded. """ - app = APP.read_text() + app = STATIC.joinpath("files-app.js").read_text() # Anchored on the loop rather than on the markup around it: the toolbar # moved from a dropdown to icon buttons and took the old wrapper with it, # while the property under test — one picker at a time — did not change. @@ -269,7 +281,7 @@ def test_links_in_chat_are_built_as_elements_not_markup(): never HTML, and only for http(s) — otherwise javascript: would be one message away from running here. """ - app = APP.read_text() + app = STATIC.joinpath("chat-app.js").read_text() fn = app[app.index("function linkify("):] fn = fn[:fn.index("\nfunction ", 1)] assert "innerHTML" not in fn and "dangerouslySetInnerHTML" not in fn diff --git a/packages/meshbay-hub/tests/test_transfers.py b/packages/meshbay-hub/tests/test_transfers.py index 35fe1cc..86d58cd 100644 --- a/packages/meshbay-hub/tests/test_transfers.py +++ b/packages/meshbay-hub/tests/test_transfers.py @@ -209,7 +209,7 @@ def test_the_colour_is_defined_for_that_mark(): def test_a_folder_name_carries_no_trailing_slash(): """The folder icon in the cell beside it already says what it is.""" - source = APP.read_text(encoding="utf-8") + source = STATIC.joinpath("files-app.js").read_text(encoding="utf-8") row = source[source.index('class="file-row dir-row"'):] row = row[:row.index("</tr>")] assert "${d}/" not in row, "the folder name is rendered with a trailing slash" 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 ────────────────────────────────────────────────────────── diff --git a/packages/meshbay-hub/tests/test_upload_controls_hidden.py b/packages/meshbay-hub/tests/test_upload_controls_hidden.py index d859125..9290a94 100644 --- a/packages/meshbay-hub/tests/test_upload_controls_hidden.py +++ b/packages/meshbay-hub/tests/test_upload_controls_hidden.py @@ -17,7 +17,15 @@ from pathlib import Path import pytest STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static" +# The group-page refactor split what used to be one app.js into one file per +# "application" plus the group shell. mayUpload itself is still derived once, +# in the shell (group-page.js) — Files and Chat each moved to their own file +# and receive it as a prop, the same shape ChatPanel already took. APP = STATIC / "app.js" +GROUP_PAGE = STATIC / "group-page.js" +FILES_APP = STATIC / "files-app.js" +CHAT_APP = STATIC / "chat-app.js" +GROUP_SETTINGS = STATIC / "group-settings.js" TRANSPORT = STATIC / "transport.js" pytestmark = pytest.mark.skipif(not APP.exists(), reason="SPA sources unavailable") @@ -25,7 +33,7 @@ pytestmark = pytest.mark.skipif(not APP.exists(), reason="SPA sources unavailabl @pytest.fixture(scope="module") def app() -> str: - return APP.read_text(encoding="utf-8") + return GROUP_PAGE.read_text(encoding="utf-8") def _component(app: str, name: str) -> str: @@ -36,15 +44,15 @@ def _component(app: str, name: str) -> str: # ── Both controls ─────────────────────────────────────────────────────────── -def test_the_files_toolbar_hides_its_upload_button(app): - page = _component(app, "GroupPage") +def test_the_files_toolbar_hides_its_upload_button(): + page = _component(FILES_APP.read_text(encoding="utf-8"), "FilesPanel") toolbar = page[page.index("file-toolbar"):] toolbar = toolbar[:toolbar.index("group.mkdir")] assert "mayUpload &&" in toolbar, "the Upload button is offered regardless" -def test_the_chat_composer_hides_its_paperclip(app): - chat = _component(app, "ChatPanel") +def test_the_chat_composer_hides_its_paperclip(): + chat = _component(CHAT_APP.read_text(encoding="utf-8"), "ChatPanel") composer = chat[chat.index("chat-input-row"):] assert "mayUpload &&" in composer, ( "the chat attachment is the second way in and is still offered") @@ -53,15 +61,19 @@ def test_the_chat_composer_hides_its_paperclip(app): def test_both_read_the_same_answer(app): """Two derivations would eventually disagree, and the disagreement would be one of them offering an upload the node refuses.""" - page = _component(app, "GroupPage") - assert re.search(r"const mayUpload = memberUpload \|\| isNodeAdmin;", page), ( + assert re.search(r"const mayUpload = memberUpload \|\| isNodeAdmin;", app), ( "mayUpload is no longer derived in one place") - assert "mayUpload=${mayUpload}" in page, "the chat panel is told separately" + # Files and Chat both receive it from the same `commonProps` object the + # shell spreads into whichever app tab is active — one derivation feeding + # one object, rather than two hand-written prop attributes that could + # drift apart. + props = app[app.index("const commonProps = {"):app.index("return html`")] + assert "mayUpload," in props or "mayUpload:" in props, ( + "mayUpload is not in the shared props object every app receives") def test_the_operator_keeps_their_own_controls(app): - page = _component(app, "GroupPage") - assert "memberUpload || isNodeAdmin" in page, ( + assert "memberUpload || isNodeAdmin" in app, ( "turning uploads off would hide the operator's own upload button") @@ -70,27 +82,24 @@ def test_the_operator_keeps_their_own_controls(app): def test_the_answer_comes_from_the_node(app): """Not from the hub, which has no say in what may be written to someone else's disk, and no way to be believed about it.""" - page = _component(app, "GroupPage") - assert "ack.member_upload !== false" in page, ( + assert "ack.member_upload !== false" in app, ( "the handshake ack is what carries this") - assert "hubFetch" not in page[page.index("ack.member_upload") - 400: - page.index("ack.member_upload")] + assert "hubFetch" not in app[app.index("ack.member_upload") - 400: + app.index("ack.member_upload")] def test_an_older_node_is_treated_as_permissive(app): """A node that predates the setting sends no such field. Reading a missing field as "off" would close every group on the older half of the network.""" - page = _component(app, "GroupPage") - assert "!== false" in page[page.index("ack.member_upload"): - page.index("ack.member_upload") + 60] + assert "!== false" in app[app.index("ack.member_upload"): + app.index("ack.member_upload") + 60] def test_a_change_reaches_people_already_connected(app): """The operator may be someone else entirely, changing it while you have the group open. A button that survives until the next reconnection is a button somebody presses.""" - page = _component(app, "GroupPage") - assert "transport.onUploadPolicy" in page + assert "transport.onUploadPolicy" in app transport = TRANSPORT.read_text(encoding="utf-8") assert "member_upload_ack" in transport, "nothing routes the node's notice" @@ -115,8 +124,8 @@ def test_changing_it_is_signed(app): "an unsigned instruction would let any member turn uploads back on") -def test_only_the_operator_is_offered_the_setting(app): - panel = _component(app, "GroupSettingsPanel") +def test_only_the_operator_is_offered_the_setting(): + panel = _component(GROUP_SETTINGS.read_text(encoding="utf-8"), "GroupSettingsPanel") section = panel[panel.index("members.uploads_title") - 400: panel.index("members.uploads_title")] assert "isNodeAdmin && connected" in section diff --git a/packages/meshbay-hub/tests/test_video_buffer_ceiling.py b/packages/meshbay-hub/tests/test_video_buffer_ceiling.py index cf38117..59f97de 100644 --- a/packages/meshbay-hub/tests/test_video_buffer_ceiling.py +++ b/packages/meshbay-hub/tests/test_video_buffer_ceiling.py @@ -45,7 +45,7 @@ from pathlib import Path import pytest STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static" -APP = STATIC / "app.js" +APP = STATIC / "video-player.js" NODE_SERVER = (Path(__file__).resolve().parents[2] / "meshbay-node" / "src" / "meshbay_node" / "transport" / "webrtc_server.py") @@ -61,7 +61,8 @@ def app(): def _player(app: str) -> str: i = app.index("function VideoPlayer(") - return app[i:app.index("\nfunction ", i + 1)] + nxt = app.find("\nfunction ", i + 1) + return app[i:nxt if nxt > 0 else len(app)] # ── The shipped functions, run against a browser that has a ceiling ─────────── diff --git a/packages/meshbay-hub/tests/test_video_seek.py b/packages/meshbay-hub/tests/test_video_seek.py index 51f65ce..85e774d 100644 --- a/packages/meshbay-hub/tests/test_video_seek.py +++ b/packages/meshbay-hub/tests/test_video_seek.py @@ -36,7 +36,7 @@ from pathlib import Path import pytest STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static" -APP = STATIC / "app.js" +APP = STATIC / "video-player.js" TRANSPORT = STATIC / "transport.js" NODE_SERVER = (Path(__file__).resolve().parents[2] / "meshbay-node" / "src" / "meshbay_node" / "transport" / "webrtc_server.py") @@ -51,7 +51,8 @@ def app(): def _player(app: str) -> str: i = app.index("function VideoPlayer(") - return app[i:app.index("\nfunction ", i + 1)] + nxt = app.find("\nfunction ", i + 1) + return app[i:nxt if nxt > 0 else len(app)] # ── The node ────────────────────────────────────────────────────────────────── diff --git a/packages/meshbay-hub/tests/test_video_stream_switch.py b/packages/meshbay-hub/tests/test_video_stream_switch.py index 464d1f7..80d9421 100644 --- a/packages/meshbay-hub/tests/test_video_stream_switch.py +++ b/packages/meshbay-hub/tests/test_video_stream_switch.py @@ -35,7 +35,7 @@ from pathlib import Path import pytest STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static" -APP = STATIC / "app.js" +APP = STATIC / "video-player.js" pytestmark = pytest.mark.skipif( shutil.which("node") is None or not APP.exists(), @@ -49,7 +49,8 @@ def app(): def _player(app: str) -> str: i = app.index("function VideoPlayer(") - return app[i:app.index("\nfunction ", i + 1)] + nxt = app.find("\nfunction ", i + 1) + return app[i:nxt if nxt > 0 else len(app)] # ── The flag that stalled everything ────────────────────────────────────────── |