mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 20:27:15 +01:00
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:
parent
ba549f8f05
commit
28ced1d39a
@ -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 == []
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user