summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests')
-rw-r--r--packages/meshbay-hub/tests/harness/scroll_probe.py4
-rw-r--r--packages/meshbay-hub/tests/harness/session_harness.mjs4
-rw-r--r--packages/meshbay-hub/tests/test_downloads.py4
-rw-r--r--packages/meshbay-hub/tests/test_hook_ordering.py41
-rw-r--r--packages/meshbay-hub/tests/test_layout_responsive.py4
-rw-r--r--packages/meshbay-hub/tests/test_resume_position.py2
-rw-r--r--packages/meshbay-hub/tests/test_session_renewal.py13
-rw-r--r--packages/meshbay-hub/tests/test_spa_ordering.py22
-rw-r--r--packages/meshbay-hub/tests/test_transfers.py2
-rw-r--r--packages/meshbay-hub/tests/test_transport_contracts.py113
-rw-r--r--packages/meshbay-hub/tests/test_upload_controls_hidden.py51
-rw-r--r--packages/meshbay-hub/tests/test_video_buffer_ceiling.py5
-rw-r--r--packages/meshbay-hub/tests/test_video_seek.py5
-rw-r--r--packages/meshbay-hub/tests/test_video_stream_switch.py5
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 ──────────────────────────────────────────