From 13e7980071ab11b07b5d473e43506c6cb4ab637e Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Mon, 12 Oct 2026 12:57:49 +0200 Subject: fix: keep the photo lightbox's close button on a phone screen The bar was 457 px wide and pushed the close button to 397..437 px, off a 360 px phone. Delete leaves the lightbox (it stays on the tile's menu: right-click, or press and hold), and below 768 px the bar drops the file name, the zoom label and 100%. Co-Authored-By: Claude Opus 5.5 --- .../tests/harness/photo_delete_probe.py | 32 ++++---------- packages/meshbay-hub/tests/test_layout_measured.py | 51 ++++++++++++++++++++++ packages/meshbay-hub/tests/test_photo_delete.py | 15 +++---- 3 files changed, 66 insertions(+), 32 deletions(-) (limited to 'packages/meshbay-hub/tests') diff --git a/packages/meshbay-hub/tests/harness/photo_delete_probe.py b/packages/meshbay-hub/tests/harness/photo_delete_probe.py index a99566d..2c41b14 100755 --- a/packages/meshbay-hub/tests/harness/photo_delete_probe.py +++ b/packages/meshbay-hub/tests/harness/photo_delete_probe.py @@ -5,8 +5,8 @@ Deleting a photo from the Photos app, in a real browser. Mounts the shipped `PhotosApp` on a made-up index, with a transport that records what it is asked to delete and an index that drops what was deleted, as a node's does. Right-clicks a tile, picks Delete, answers the page's own -confirmation (ask.js) one way then the other, and does the same from the -lightbox's bar. +confirmation (ask.js) one way then the other, and checks the lightbox's bar +offers none. photo_delete_probe.py @@ -109,16 +109,8 @@ const pick = async (label) => { await wait(50); return true; }; -// The page's own confirmation: what it says, whether it is the topmost thing -// on screen, and an answer. +// The page's own confirmation: what it says, and an answer. const dialog = () => document.querySelector('[role="alertdialog"]'); -const onTop = () => { - const d = dialog(); - if (!d) return false; - const r = d.getBoundingClientRect(); - const hit = document.elementFromPoint(r.left + r.width / 2, r.top + r.height / 2); - return Boolean(hit && d.contains(hit)); -}; const answer = async (yes) => { const d = dialog(); const buttons = [...d.querySelectorAll('button')]; @@ -172,21 +164,12 @@ const answer = async (yes) => { await closeMenus(); cases.push({ case: 'operator', delete_offered: all }); - // -- from the lightbox: the button, the dialog above it, the next photo -- + // -- the lightbox: no delete button, the tile is where it is -- await mount({}); document.querySelectorAll('.photo-tile')[0].click(); await frame(); - const title = () => (document.querySelector('.photo-lightbox .video-title') || {}).textContent; - const before = title(); - const btn = document.querySelector('.photo-lightbox button[aria-label="Delete"]'); - if (btn) btn.click(); - await wait(50); - const dialogOnTop = onTop(); - if (dialog()) await answer(true); - await wait(100); await frame(); - cases.push({ case: 'lightbox', button: Boolean(btn), dialog_on_top: dialogOnTop, - before, after: title() || null, open: Boolean(document.querySelector('.photo-lightbox')), - deleted: DELETED.map((d) => d[0]) }); + cases.push({ case: 'lightbox', open: Boolean(document.querySelector('.photo-lightbox')), + button: Boolean(document.querySelector('.photo-lightbox button[aria-label="Delete"]')) }); // -- read-only (Search): never -- await mount({ readOnly: true, isNodeAdmin: true }); @@ -196,7 +179,8 @@ const answer = async (yes) => { await closeMenus(); ro.click(); await frame(); cases.push({ case: 'read only', labels: roLabels, - lightbox_button: Boolean(document.querySelector('.photo-lightbox button[aria-label="Delete"]')) }); + lightbox_button: Boolean( + document.querySelector('.photo-lightbox button[aria-label="Delete"]')) }); parent.postMessage({ cases, logs: LOGS, origin: location.origin }, '*'); } catch (err) { diff --git a/packages/meshbay-hub/tests/test_layout_measured.py b/packages/meshbay-hub/tests/test_layout_measured.py index 298a754..96f035f 100644 --- a/packages/meshbay-hub/tests/test_layout_measured.py +++ b/packages/meshbay-hub/tests/test_layout_measured.py @@ -362,3 +362,54 @@ def test_the_name_leaves_the_trigger_on_mobile_only(full_nav, width): assert shown == (width > 768), ( f"at {width} px the name in the user menu is " f"{'shown' if shown else 'hidden'}") + + +# The photo lightbox's bar. With the file name, the zoom label, 100% and a +# delete button it was 457 px wide, and the close button sat at 397..437 px — +# off a 360 px phone, with no other way out of the lightbox on screen. The +# fixture is `Lightbox()`'s bar, cast button and copy-link included. +_ZOOM_ICON = '' +LIGHTBOX_BAR = textwrap.dedent(f""" +
+
+ IMG_20150510_205012.JPG +
+ + Ajusté + + + +
+ +
+ + +
+
+""") +LIGHTBOX_WIDTHS = [320, 360, 412, 768, 1024] + + +@pytest.fixture(scope="module") +def lightbox_bar(tmp_path_factory): + fragment = tmp_path_factory.mktemp("lightbox") / "fragment.html" + fragment.write_text(LIGHTBOX_BAR, encoding="utf-8") + proc = subprocess.run( + ["python3", str(HARNESS), ",".join(str(w) for w in LIGHTBOX_WIDTHS), + str(fragment), ".lightbox-close", ".video-title"], + capture_output=True, text=True, timeout=180) + assert proc.returncode == 0, f"probe failed: {proc.stdout}{proc.stderr}" + out = json.loads(proc.stdout) + assert "error" not in out, f"no measurement: {out}" + return out + + +@pytest.mark.parametrize("width", LIGHTBOX_WIDTHS) +def test_the_lightbox_close_button_is_on_screen(lightbox_bar, width): + close = lightbox_bar[str(width)]["boxes"][".lightbox-close"] + assert close["right"] <= width, ( + f"at {width} px the lightbox's close button ends at {close['right']} px") + + +def test_the_file_name_stays_in_the_desktop_lightbox(lightbox_bar): + assert lightbox_bar["1024"]["boxes"][".video-title"]["width"] > 200 diff --git a/packages/meshbay-hub/tests/test_photo_delete.py b/packages/meshbay-hub/tests/test_photo_delete.py index bcc943f..bd22cbe 100644 --- a/packages/meshbay-hub/tests/test_photo_delete.py +++ b/packages/meshbay-hub/tests/test_photo_delete.py @@ -1,10 +1,11 @@ """ Deleting a photo from the Photos app (harness/photo_delete_probe.py). -On a right-clicked tile and in the lightbox's bar, which is how a phone gets -at it. Offered on the rule Files applies and the node enforces: the node's -operator, or whoever uploaded the photo. Never without the page's own -confirmation, and never from Search, which is read-only. +On a tile's menu — right-click, or press and hold on a phone — and not in the +lightbox's bar, where it pushed the close button off a phone's screen. +Offered on the rule Files applies and the node enforces: the node's operator, +or whoever uploaded the photo. Never without the page's own confirmation, and +never from Search, which is read-only. """ import json @@ -48,11 +49,9 @@ def test_the_operator_may_delete_any_photo(cases): assert cases["operator"]["delete_offered"] == [True, True, True] -def test_the_lightbox_deletes_and_shows_the_next_photo(cases): +def test_the_lightbox_offers_no_delete(cases): c = cases["lightbox"] - assert c["button"] and c["dialog_on_top"] - assert c["deleted"] == ["p1"] - assert c["open"] and c["before"] == "beach.jpg" and c["after"] == "dunes.jpg" + assert c["open"] and not c["button"] def test_search_results_offer_no_delete(cases): -- cgit v1.2.3