aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_app_settings_plugin.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-07 01:50:14 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-07 01:50:14 +0200
commit375419c041a4523614b3a1b98fa54fb5acb1b8bf (patch)
tree51086cd1e29a3f78f352dc2080847392a42fc37a /packages/meshbay-hub/tests/test_app_settings_plugin.py
parent435f54b382004de28196aa44c9b1d2c7368ae212 (diff)
downloadmeshbay-375419c041a4523614b3a1b98fa54fb5acb1b8bf.tar.gz
fix(client): Save in the app panes was invisible, and Chat's never landed
Two defects behind one report — "you cannot always click Save, you do not notice, and it does not work". **It did not work, for Chat, systematically.** `_dispatch` resolves an admin ack against the pending request and returns, which is right for an op whose caller already knows the value it chose. Chat's pane calls `transport.setChatDirectory` itself, so nothing told `group-page` anything: the node saved it, every *other* connected client learned it from the broadcast, and the one that asked went on showing an unsaved-looking draft. Clicking Save again just re-sent it. Same shape as the root-ack bug, in a different message — so the set is now `BROADCAST_ACK_TYPES`, named for the property that makes it true, and covers `chat_directory`, `chat_link_preview` and `app_directories`. **You could not notice, because Save was not visibly a button.** It carried `btn btn-small btn-secondary`, and there is no `.btn` rule in the stylesheet at all — so it took `.btn-secondary`: no background, a transparent border, dim grey text. Enabled it already looked like a disabled control; disabled it was the same thing at 40% opacity. Measured on a real page: enabled is now accent on white, disabled is grey text on a plain border, and an inert one says why ("No changes to save") rather than leaving the reader to guess what the pane counts as a change. Videos' second Save — the TMDB one — is the same control, because two Save buttons in one pane that do not look alike is worse than either looking wrong. Verified by driving the real pane in Electron through the whole cycle: inert, pick a folder, live, save, and the node's answer coming back to disable it again. Four earlier readings said the enabled button was transparent; all four were taken inside python's directory-listing page, which the app never runs in — my scaffolding, not the code. Hub suite only: the node package is untouched by this. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011pvMdvLBG92jyhvD5pD6us
Diffstat (limited to 'packages/meshbay-hub/tests/test_app_settings_plugin.py')
-rw-r--r--packages/meshbay-hub/tests/test_app_settings_plugin.py72
1 files changed, 72 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_app_settings_plugin.py b/packages/meshbay-hub/tests/test_app_settings_plugin.py
index 6a768a9..8a095af 100644
--- a/packages/meshbay-hub/tests/test_app_settings_plugin.py
+++ b/packages/meshbay-hub/tests/test_app_settings_plugin.py
@@ -320,3 +320,75 @@ def test_a_refusal_does_not_look_like_a_footnote():
assert "role=" in block and "alert" in block, (
"a refusal that appears after a click has to be announced, not just "
"drawn")
+
+
+# ── Saving, and being able to tell ──────────────────────────────────────────
+
+PANE_FLAGS = {"chat-app-settings.js": "dirty",
+ "video-app-settings.js": "dirsDirty",
+ "music-app-settings.js": "dirsDirty",
+ "photos-app-settings.js": "dirty"}
+
+
+@pytest.mark.parametrize("pane", sorted(PANE_FLAGS))
+def test_save_is_a_button_and_not_dim_text(pane):
+ """
+ It was `btn btn-small btn-secondary`, and there is no `.btn` rule in the
+ stylesheet at all — so it took `.btn-secondary`: no background, a
+ transparent border, dim grey text. Enabled it already looked like a
+ disabled control, and disabled it was that at 40% opacity.
+
+ "You cannot always click Save, you do not notice, and it does not work" is
+ one sentence describing all of that.
+ """
+ source = (STATIC / pane).read_text(encoding="utf-8")
+ assert 'class="app-save"' in source, f"{pane}'s Save is not the shared control"
+ assert "btn-secondary" not in source, (
+ f"{pane}'s Save is still styled as dim text")
+
+
+@pytest.mark.parametrize("pane", sorted(PANE_FLAGS))
+def test_an_inert_save_says_why(pane):
+ """
+ A disabled control that explains itself is the difference between "nothing
+ changed" and "this is broken". Without it the reader has to know what the
+ pane counts as a change.
+ """
+ source = (STATIC / pane).read_text(encoding="utf-8")
+ assert "app-save-why" in source and "settings_app.no_changes" in source
+
+
+def test_the_disabled_state_is_visually_distinct():
+ css = (STATIC / "style.css").read_text(encoding="utf-8")
+ rule = css[css.index(".app-save {"):]
+ rule = rule[:rule.index(".app-save-row")]
+ assert "var(--accent)" in rule, "an enabled Save has no fill"
+ disabled = rule[rule.index(".app-save:disabled"):]
+ assert "background: none" in disabled and "--text-dim" in disabled, (
+ "disabled differs from enabled by opacity alone, which is what made "
+ "it unreadable")
+
+
+def test_a_saved_setting_reaches_the_page_that_renders_the_pane():
+ """
+ Why Chat was the systematic case.
+
+ `_dispatch` resolves an admin ack against the pending request and returns —
+ right for an op whose caller already knows the value it chose. Chat's pane
+ calls `transport.setChatDirectory` itself, so nothing told `group-page`
+ anything: the node saved it, every *other* connected client learned it from
+ the broadcast, and the one that asked went on showing an unsaved-looking
+ draft. Clicking Save again just re-sent it.
+ """
+ transport = TRANSPORT.read_text(encoding="utf-8")
+ block = transport[transport.index("const BROADCAST_ACK_TYPES"):]
+ block = block[:block.index("]);") + 3]
+ for ack in ("chat_directory_ack", "chat_link_preview_ack",
+ "app_directories_ack"):
+ assert ack in block, f"{ack} is swallowed by its own request"
+
+ assert "_replayBroadcast" in transport
+ replay = transport[transport.index("function _replayBroadcast"):]
+ replay = replay[:replay.index("\n}") + 2]
+ for cb in ("_onChatDirectory", "_onChatLinkPreview", "_onAppDirectories"):
+ assert cb in replay, f"{cb} is never called for the requester"