Cover the last branches of the viewport, the density scale and the keys

gatelock has never met its own 100% bar; _scrollable.py in particular shipped
with no tests at all, which is how a viewport that scrolled itself passed
every check for a week. The remaining gaps were the ones a reader would most
want proven:

* finalize() is documented idempotent -- a repaint calls it again, and an
  earlier version stacked a <FocusIn> handler per call.
* scroll_into_view holds still for a widget already fully on screen. Its
  margin logic has three outcomes and "hold" is the one a jittering viewport
  gets wrong.
* the type scale reads an existing Tk root rather than opening a second one
  next to a live lock window.
* bind_cancel rejects a non-callable at bind time, not on the keypress that
  finally needed Escape to work.

328 tests, coverage 99.44% -> 100.00%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BsXnTHyuuCUZsqetTaajcF
This commit is contained in:
Krzysztof Rudnicki 2026-08-03 16:20:11 +02:00
parent ba549f8f05
commit 28ced1d39a
3 changed files with 81 additions and 0 deletions

View File

@ -185,3 +185,23 @@ def test_forced_restores_a_pre_existing_override(
with _density.forced(0.5):
assert _density.density() == 0.5
assert _density.density() == 0.9
def test_an_existing_root_is_asked_rather_than_a_new_one(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""With a root already up, the factor comes from it -- no second Tk.
The apps that build their LockConfig lazily hit this path, and creating a
throwaway Tk alongside a live lock window is not something to do casually.
"""
destroyed: list[bool] = []
stub = types.SimpleNamespace(
_default_root=_StubRoot(768, destroyed),
Tk=None,
TclError=tk.TclError,
)
monkeypatch.setattr(_density, "tk", stub)
assert _density._screen_height() == 768
assert destroyed == []

View File

@ -100,3 +100,13 @@ def test_shift_tab_goes_back_out_of_a_text_box(root: tk.Tk) -> None:
root.update()
assert root.focus_get() is before
def test_a_non_callable_cancel_is_rejected_at_bind_time(root: tk.Tk) -> None:
"""Fail where the mistake is, not on the keypress that needed to work.
Escape is a lock's sanctioned way back; a binding that raises only when
the user finally presses it fails at the worst possible moment.
"""
with pytest.raises(TypeError, match="callback must be callable"):
bind_cancel(root, "not a function")

View File

@ -276,3 +276,54 @@ def test_paging_degrades_to_the_canvas_without_a_toplevel(
surface.canvas.event_generate("<Next>", when="now")
surface.canvas.update()
assert _offset(surface) > 0.0
def test_a_second_finalize_does_not_stack_handlers(
surface: ScrollableSurface,
) -> None:
"""finalize() is documented idempotent, and a repaint calls it again.
An earlier version re-bound <FocusIn> on every call, so handlers piled up
one per repaint across the ~19 repaint sites of a single lock session.
"""
bound_once = set(surface._focus_bound)
surface.finalize()
assert set(surface._focus_bound) == bound_once
def test_scroll_into_view_does_nothing_when_everything_fits() -> None:
"""Nothing to reveal on a screen that is fully visible already."""
root = tk.Tk()
try:
root.geometry(f"400x{_VIEWPORT_H}+0+0")
root.focus_force()
root.update_idletasks()
made = ScrollableSurface(root, LockConfig())
made.container.place(relx=0, rely=0, relwidth=1, relheight=1)
entry = tk.Entry(made.content)
entry.pack()
made.finalize()
root.update()
made.scroll_into_view(entry)
assert made.canvas.yview() == (0.0, 1.0)
finally:
root.destroy()
def test_scroll_into_view_holds_still_for_a_widget_in_the_middle(
surface: ScrollableSurface,
) -> None:
"""A widget already fully on screen is not dragged to an edge.
The margin logic has three outcomes -- scroll up, scroll down, hold -- and
"hold" is the one a viewport that jitters would get wrong.
"""
middle = surface.content.winfo_children()[3]
before = _offset(surface)
surface.scroll_into_view(middle)
assert _offset(surface) == before