diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-18 14:07:16 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-18 14:07:16 +0200 |
| commit | c384878aa0d6ef7a33bda23887dc264b94386725 (patch) | |
| tree | 67279ddc7cfbb0902b0cc1348bb66c2724a63d36 /packages/meshbay-hub/tests | |
| parent | cf418a095b07c8127042390c748e721d1433879b (diff) | |
| download | meshbay-c384878aa0d6ef7a33bda23887dc264b94386725.tar.gz | |
fix(client): a film can go full-screen, and automatic saving is automatic
**Full-screen was denied, and the denial was invisible.** The permission
handler was written from a true sentence — nothing here needs a camera, a
microphone or a location — and implemented as `callback(false)` for everything.
Chromium's own video controls ask for the `fullscreen` permission, so a film
could not be watched full-screen.
What made it hard to find, and what the test now pins: **a denied `fullscreen`
does not reject.** `requestFullscreen()` returns a promise that never settles.
No exception, no console message, nothing in the renderer that mentions a
permission — the button just does nothing. Measured rather than reasoned: the
probe reported `NEVER SETTLED` while the main process, instrumented for one run,
logged `PERMISSION ASKED: fullscreen`. After the fix the same probe reports
`granted` with `document.fullscreenElement` set.
The handler now enumerates what is *granted* — `fullscreen`, and nothing else —
so a camera, a microphone, a location, notifications and MIDI are still refused
and whatever Chromium adds next arrives refused rather than quietly allowed.
`Permissions.query` takes the other handler, so both now answer from the one
list instead of eventually disagreeing.
The old test asserted `callback(false)`, which is to say it locked in the bug.
It is replaced by three: what must stay denied, that `fullscreen` is granted,
and that both handlers read the same list.
**"Save automatically" opened a dialog.** The automatic path required a folder
to have been chosen first, and on a new profile nobody has chosen one — so the
very first download fell through to Save As, which is the one thing the setting
promises not to do. A browser does not make you pick a folder before it will
save a file; the system Downloads folder is the answer when there is no other.
Verified on a fresh profile with a home of its own: no dialog, 1024 bytes on
disk, destination reported as the default (`/home/…/Téléchargements` on this
machine, via the localized XDG directory).
A folder that *was* chosen and has since gone still asks. Silently redirecting
those files is worse than a dialog: someone who picked an external drive wants
to be told it is not there, not to find the film in their home directory a week
later. Settings shows the effective destination either way, and offers "forget"
only for a folder somebody actually chose.
813 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/tests')
| -rw-r--r-- | packages/meshbay-hub/tests/test_desktop_shell.py | 65 |
1 files changed, 61 insertions, 4 deletions
diff --git a/packages/meshbay-hub/tests/test_desktop_shell.py b/packages/meshbay-hub/tests/test_desktop_shell.py index c4f5b08..0abdb68 100644 --- a/packages/meshbay-hub/tests/test_desktop_shell.py +++ b/packages/meshbay-hub/tests/test_desktop_shell.py @@ -13,6 +13,7 @@ Every assertion here corresponds to a sentence in `docs/desktop-client-v1.md` person with an installed client is what confirms the rest. """ +import re from pathlib import Path import pytest @@ -69,10 +70,40 @@ def test_navigation_away_from_the_package_is_refused(): assert "event.preventDefault()" in source -def test_no_permission_is_granted_to_the_page(): - """Nothing here needs a camera, a microphone or a location.""" - assert "setPermissionRequestHandler" in _main() - assert "callback(false)" in _main() +def _granted_permissions() -> set[str]: + """The allowlist, read out of the source rather than described here.""" + match = re.search(r"GRANTED_PERMISSIONS = new Set\(\[([^\]]*)\]\)", _main()) + assert match, "the permission allowlist is gone or was renamed" + return set(re.findall(r"'([^']+)'", match.group(1))) + + +def test_the_page_gets_no_camera_microphone_or_location(): + denied = {"media", "geolocation", "midi", "midiSysex", "notifications", + "pointerLock", "openExternal", "clipboard-read", "hid", "serial", + "usb", "idle-detection", "window-management"} + assert not (_granted_permissions() & denied) + + +def test_video_may_go_fullscreen(): + """ + The regression this replaced a blanket denial to fix, and the reason it is + worth a test: **a denied `fullscreen` does not reject.** Chromium's own + video controls ask for it, `requestFullscreen()` returns a promise that + never settles, and the button does nothing with no error raised anywhere. + Nothing observable says "permission" — so nothing would have led back here. + """ + assert "fullscreen" in _granted_permissions() + + +def test_both_permission_handlers_answer_from_the_same_list(): + """`Permissions.query` takes the check handler and a request takes the + other; two lists would eventually disagree about what the page may do.""" + source = _main() + for handler in ("setPermissionRequestHandler", "setPermissionCheckHandler"): + assert handler in source + after = source.split(handler, 1)[1][:400] + assert "GRANTED_PERMISSIONS" in after, ( + f"{handler} decides on its own rather than from the allowlist") # ── The custom scheme ─────────────────────────────────────────────────────── @@ -241,3 +272,29 @@ def test_the_packaged_page_loads_the_shared_modules(): "style.css", "argon2.min.js"): assert module in page, f"{module} is not loaded by the packaged page" assert "/a/" not in page, "the packaged page points at the hub's asset prefix" + + +# ── Where downloads land ──────────────────────────────────────────────────── + +def test_automatic_saving_never_opens_a_dialog_for_want_of_a_folder(): + """ + "Save automatically" opened a Save As dialog on the first download, because + the automatic path required a folder to have been chosen first and nobody + had chosen one. A browser does not ask before it will save a file; the + system Downloads folder is the answer when there is no other. + """ + source = _main() + begin = source.split("ipcMain.handle('save:begin'", 1)[1].split("ipcMain.handle", 1)[0] + assert "defaultDownloadDir()" in begin, ( + "the automatic path has no destination when no folder was chosen") + assert "app.getPath('downloads')" in source + + +def test_a_chosen_folder_that_has_gone_is_not_silently_replaced(): + """Someone who picked an external drive should be told it is not there, + not find the film in their home directory a week later.""" + source = _main() + begin = source.split("ipcMain.handle('save:begin'", 1)[1].split("ipcMain.handle", 1)[0] + assert "config.downloadDir && !chosen" in begin, ( + "a chosen-but-missing folder falls through to the default instead of asking") + assert "showSaveDialog" in begin |