init_colors() now survives a terminal rejecting every init_pair() call (some serial-console terminfo entries claim start_color()/COLORS support but can't actually set custom pairs), but that exposed a follow-on bug: the list cursor and the cert-form Continue/Cancel buttons indicated selection purely via the COLOR_SELECTED color pair, so once that pair silently no-ops the current row/button becomes indistinguishable from an unselected one. Add curses.A_REVERSE alongside the color pair for the selected state — a plain video attribute that doesn't depend on custom-pair support, so the cursor stays visible either way. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
97 lines
3.4 KiB
Python
97 lines
3.4 KiB
Python
from unittest.mock import MagicMock, patch
|
|
import curses as _curses
|
|
|
|
# Stub just enough of the curses API for InputField to import
|
|
_curses.color_pair = lambda x: 0
|
|
_curses.A_UNDERLINE = 0
|
|
_curses.A_BOLD = 0
|
|
_curses.KEY_BACKSPACE = 263
|
|
_curses.KEY_LEFT = 260
|
|
_curses.KEY_RIGHT = 261
|
|
_curses.KEY_HOME = 262
|
|
_curses.KEY_END = 360
|
|
_curses.KEY_DC = 330
|
|
|
|
from openvpncertupdate import InputField, clamp, init_colors, COLOR_NORMAL, COLOR_DISABLED
|
|
|
|
|
|
def _field(initial="", mask=False):
|
|
win = MagicMock()
|
|
win.getmaxyx.return_value = (1, 80)
|
|
return InputField(win, 0, 0, 40, initial=initial, mask=mask)
|
|
|
|
|
|
def test_initial_value():
|
|
assert _field("hello").value == "hello"
|
|
|
|
def test_backspace_removes_last():
|
|
f = _field("hello")
|
|
f.handle_key(_curses.KEY_BACKSPACE)
|
|
assert f.value == "hell"
|
|
|
|
def test_type_appends():
|
|
f = _field("hi")
|
|
f.handle_key(ord("!"))
|
|
assert f.value == "hi!"
|
|
|
|
def test_left_then_insert():
|
|
f = _field("ac")
|
|
f.handle_key(_curses.KEY_LEFT)
|
|
f.handle_key(_curses.KEY_LEFT)
|
|
f.handle_key(ord("b"))
|
|
assert f.value == "bac"
|
|
|
|
def test_delete_key():
|
|
f = _field("abc")
|
|
f.handle_key(_curses.KEY_HOME)
|
|
f.handle_key(_curses.KEY_DC)
|
|
assert f.value == "bc"
|
|
|
|
def test_clamp_bounds():
|
|
assert clamp(5, 0, 10) == 5
|
|
assert clamp(-1, 0, 10) == 0
|
|
assert clamp(11, 0, 10) == 10
|
|
|
|
|
|
def _init_colors_calls(*, use_default_colors_raises, colors):
|
|
calls = []
|
|
use_default = MagicMock(
|
|
side_effect=_curses.error("use_default_colors() returned ERR")
|
|
if use_default_colors_raises else None
|
|
)
|
|
with patch.object(_curses, "start_color"), \
|
|
patch.object(_curses, "use_default_colors", use_default), \
|
|
patch.object(_curses, "init_pair", side_effect=lambda *a: calls.append(a)), \
|
|
patch.object(_curses, "COLORS", colors, create=True):
|
|
init_colors()
|
|
return calls
|
|
|
|
def test_init_colors_uses_default_colors_when_supported():
|
|
calls = _init_colors_calls(use_default_colors_raises=False, colors=256)
|
|
normal = next(c for c in calls if c[0] == COLOR_NORMAL)
|
|
disabled = next(c for c in calls if c[0] == COLOR_DISABLED)
|
|
assert normal[2] == -1
|
|
assert disabled[1:] == (8, -1)
|
|
|
|
def test_init_colors_falls_back_on_serial_console():
|
|
# Serial consoles (e.g. TERM=linux/vt100) often lack the terminfo
|
|
# "op"/"AX" capability, so use_default_colors() raises curses.error
|
|
# and only 8 colors are available.
|
|
calls = _init_colors_calls(use_default_colors_raises=True, colors=8)
|
|
normal = next(c for c in calls if c[0] == COLOR_NORMAL)
|
|
disabled = next(c for c in calls if c[0] == COLOR_DISABLED)
|
|
assert normal[2] == _curses.COLOR_BLACK
|
|
assert disabled[1:] == (_curses.COLOR_WHITE, _curses.COLOR_BLACK)
|
|
|
|
def test_init_colors_survives_init_pair_rejecting_every_pair():
|
|
# Some serial-console terminfo entries claim start_color()/COLORS
|
|
# support but reject every init_pair() call (e.g. COLOR_PAIRS too
|
|
# small). init_colors() must not raise -- the app should still start,
|
|
# just without custom colors.
|
|
with patch.object(_curses, "start_color"), \
|
|
patch.object(_curses, "use_default_colors"), \
|
|
patch.object(_curses, "init_pair",
|
|
side_effect=_curses.error("init_pair() returned ERR")), \
|
|
patch.object(_curses, "COLORS", 256, create=True):
|
|
init_colors() # must not raise
|