diff --git a/scripts/layout-probe.py b/scripts/layout-probe.py index 6b89f30..7cbe5b1 100755 --- a/scripts/layout-probe.py +++ b/scripts/layout-probe.py @@ -69,6 +69,24 @@ def probe(page, url: str) -> list[str]: card.hover(timeout=1500) except Exception: pass + # ⚠ OPEN EVERY
FIRST. A control inside a CLOSED one is laid out + # but sits outside its collapsed parent's box, so `elementFromPoint` at its + # centre returns an ancestor and it reports OCCLUDED — 23 of them on + # `sindra-set`, every one a false positive, because the only way an operator + # reaches that button is by opening the disclosure first. Verified both + # ways: closed -> elementFromPoint returns div.gallery; opened -> the button + # itself, and a real trial click lands on it. + # + # Opening rather than SKIPPING is deliberate. Skipping would make the probe + # quiet by declaring put-away controls out of scope, and the add-note button + # inside `details.item-addnote` is exactly the kind of control this + # instrument exists to check. Open it and ask the real question. + for d in page.locator("details:not([open])").all(): + try: + d.evaluate("(el) => el.open = true") + except Exception: + pass + page.wait_for_timeout(150) for el in page.locator("button, a.dl-link, a.thumb").all(): try: # ⚠ elementFromPoint is VIEWPORT-relative. Without scrolling first, diff --git a/tests/test_cli.py b/tests/test_cli.py index 2db4e2a..5c140d2 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -207,3 +207,47 @@ def test_the_link_board_announces_itself_as_the_booths_own(tmp_path): m = _manifest(tmp_path / "links") assert m is not None and m.handle == "booth" assert m.why + + +def test_the_flags_can_sit_on_either_side_of_the_files(tmp_path): + """`booth add b *.png --why "..."` and `booth add b --why "..." *.png` both + work. A glob is usually last and a flag usually after it, but nothing + enforces that and a session should not have to remember which.""" + src = tmp_path / "a.png" + src.write_bytes(b"x") + env = {**os.environ, "ALTHING_HANDLE": "booth-dev", + "BOOTH_DATA_DIR": str(tmp_path), "BOOTH_URL": "http://booth.invalid"} + for name, args in (("after", ["add", "after", str(src), "--why", "w"]), + ("before", ["add", "before", "--why", "w", str(src)])): + r = subprocess.run([str(SCRIPT), *args], capture_output=True, text=True, + timeout=30, env=env) + assert r.returncode == 0, r.stderr + assert _manifest(tmp_path / name).why == "w" + assert (tmp_path / name / "a.png").exists(), "the files stopped being copied" + + +def test_a_why_survives_quotes_and_non_ascii_and_is_flattened(tmp_path): + """The reason this goes through manifest.py instead of printf-ing JSON from + the shell: a why containing a quote, a backslash or a newline is not an edge + case, it is a sentence somebody wrote. Newlines flatten because the field + renders inside a card's sub-line.""" + r = subprocess.run( + [str(SCRIPT), "new", "b", "--why", 'he said "pick v3" — line1\nline2 · ünï'], + capture_output=True, text=True, timeout=30, + env={**os.environ, "ALTHING_HANDLE": "booth-dev", + "BOOTH_DATA_DIR": str(tmp_path), "BOOTH_URL": "http://booth.invalid"}) + assert r.returncode == 0, r.stderr + why = _manifest(tmp_path / "b").why + assert why == 'he said "pick v3" — line1 line2 · ünï' + + +def test_a_flag_with_no_value_does_not_eat_the_booth_name(tmp_path): + """`booth new b --why` with nothing after it must not consume `b` as the + value and then create a booth called nothing. Usage, and no directory.""" + r = subprocess.run([str(SCRIPT), "new", "b", "--why"], capture_output=True, + text=True, timeout=30, + env={**os.environ, "BOOTH_DATA_DIR": str(tmp_path), + "BOOTH_URL": "http://booth.invalid"}) + assert r.returncode == 2 + assert "usage:" in r.stderr + assert not (tmp_path / "b").exists()