@@ -331,6 +331,63 @@ def test_output_string_embedded_null_chars(self):
331331 self .assertRaises (ValueError , stdscr .insstr , arg )
332332 self .assertRaises (ValueError , stdscr .insnstr , arg , 1 )
333333
334+ def test_add_string_behavior (self ):
335+ # addstr() advances the cursor past the written text; addnstr()
336+ # writes at most n characters.
337+ win = curses .newwin (1 , 10 , 0 , 0 )
338+ win .addstr (0 , 0 , 'abc' )
339+ self .assertEqual (win .getyx (), (0 , 3 ))
340+ win .erase ()
341+ win .addnstr (0 , 0 , 'abcdef' , 3 )
342+ self .assertEqual (win .instr (0 , 0 ), b'abc ' )
343+
344+ def test_insert_string_behavior (self ):
345+ # insstr()/insnstr() insert at the cursor, shift the rest of the
346+ # line right (losing characters off the edge), and leave the cursor
347+ # where it was.
348+ win = curses .newwin (1 , 10 , 0 , 0 )
349+ win .addstr (0 , 0 , 'abcde' )
350+ win .move (0 , 1 )
351+ win .insstr ('XY' )
352+ self .assertEqual (win .getyx (), (0 , 1 )) # cursor did not advance
353+ self .assertEqual (win .instr (0 , 0 ), b'aXYbcde ' )
354+
355+ win .erase ()
356+ win .addstr (0 , 0 , 'ZZZZZ' )
357+ win .move (0 , 0 )
358+ win .insnstr ('abcdef' , 3 ) # at most 3 characters
359+ self .assertEqual (win .instr (0 , 0 ), b'abcZZZZZ ' )
360+
361+ def test_insch (self ):
362+ # insch() inserts a single character at the cursor (or at y, x),
363+ # shifting the rest of the line right.
364+ win = curses .newwin (2 , 10 , 0 , 0 )
365+ win .addstr (0 , 0 , 'abc' )
366+ win .move (0 , 1 )
367+ win .insch (ord ('X' ))
368+ self .assertEqual (win .instr (0 , 0 ), b'aXbc ' )
369+ win .insch (1 , 0 , 'Y' , curses .A_BOLD )
370+ self .assertEqual (win .inch (1 , 0 ), b'Y' [0 ] | curses .A_BOLD )
371+
372+ def test_pad (self ):
373+ pad = curses .newpad (10 , 20 )
374+ pad .addstr (0 , 0 , 'PADTEXT' )
375+ self .assertEqual (pad .instr (0 , 0 , 7 ), b'PADTEXT' )
376+
377+ # subpad() shares the parent pad's character cells.
378+ sub = pad .subpad (3 , 5 , 0 , 0 )
379+ self .assertEqual (sub .getmaxyx (), (3 , 5 ))
380+ self .assertEqual (sub .instr (0 , 0 , 5 ), b'PADTE' )
381+
382+ # A pad is refreshed onto an explicit screen rectangle; the
383+ # 6-argument form is required (and rejected for ordinary windows).
384+ pad .refresh (0 , 0 , 0 , 0 , 4 , 10 )
385+ pad .noutrefresh (0 , 0 , 0 , 0 , 4 , 10 )
386+ curses .doupdate ()
387+ self .assertRaises (curses .error , pad .refresh )
388+ win = curses .newwin (5 , 5 , 0 , 0 )
389+ self .assertRaises (TypeError , win .refresh , 0 , 0 , 0 , 0 , 4 , 4 )
390+
334391 def test_read_from_window (self ):
335392 stdscr = self .stdscr
336393 stdscr .addstr (0 , 1 , 'ABCD' , curses .A_BOLD )
@@ -347,6 +404,25 @@ def test_read_from_window(self):
347404 self .assertRaises (ValueError , stdscr .instr , - 2 )
348405 self .assertRaises (ValueError , stdscr .instr , 0 , 2 , - 2 )
349406
407+ def test_coordinate_errors (self ):
408+ # Addressing a cell outside the window raises curses.error.
409+ win = curses .newwin (5 , 10 , 0 , 0 )
410+ self .assertRaises (curses .error , win .move , 100 , 100 )
411+ self .assertRaises (curses .error , win .move , - 1 , - 1 )
412+ self .assertRaises (curses .error , win .addch , 100 , 100 , ord ('x' ))
413+ self .assertRaises (curses .error , win .chgat , 100 , 0 , curses .A_BOLD )
414+
415+ def test_argument_errors (self ):
416+ win = curses .newwin (5 , 10 , 0 , 0 )
417+ # A character argument must be an int, a byte or a one-element string.
418+ self .assertRaises (TypeError , win .addch , [])
419+ self .assertRaises (OverflowError , win .addch , 2 ** 64 )
420+ # A string method rejects a non-string, non-bytes argument.
421+ self .assertRaises (TypeError , win .addstr , 5 )
422+ self .assertRaises (TypeError , win .addstr )
423+ # Wrong number of positional arguments.
424+ self .assertRaises (TypeError , win .instr , 0 , 0 , 0 , 0 )
425+
350426 def test_getch (self ):
351427 win = curses .newwin (5 , 12 , 5 , 2 )
352428
@@ -816,6 +892,10 @@ def test_prog_mode(self):
816892 self .skipTest ('requires terminal' )
817893 curses .def_prog_mode ()
818894 curses .reset_prog_mode ()
895+ # def_shell_mode()/reset_shell_mode() are intentionally not exercised
896+ # here: they capture and restore curses' "shell mode" terminal state,
897+ # which is only meaningful before initscr(). Calling them mid-suite
898+ # corrupts the modes that endwin() restores and breaks later tests.
819899
820900 def test_beep (self ):
821901 if (curses .tigetstr ("bel" ) is not None
@@ -1028,7 +1108,8 @@ def test_keyname(self):
10281108
10291109 @requires_curses_func ('has_key' )
10301110 def test_has_key (self ):
1031- curses .has_key (13 )
1111+ self .assertIsInstance (curses .has_key (13 ), bool )
1112+ self .assertIsInstance (curses .has_key (curses .KEY_LEFT ), bool )
10321113
10331114 @requires_curses_func ('getmouse' )
10341115 def test_getmouse (self ):
@@ -1080,6 +1161,200 @@ def test_disallow_instantiation(self):
10801161 panel = curses .panel .new_panel (w )
10811162 check_disallow_instantiation (self , type (panel ))
10821163
1164+ @requires_curses_func ('panel' )
1165+ def test_panel_stack (self ):
1166+ panel = curses .panel
1167+ # new_panel() puts the panel on top of the stack, so the three
1168+ # panels end up ordered bottom -> top as p1, p2, p3.
1169+ p1 = panel .new_panel (curses .newwin (3 , 6 , 0 , 0 ))
1170+ p2 = panel .new_panel (curses .newwin (3 , 6 , 1 , 1 ))
1171+ p3 = panel .new_panel (curses .newwin (3 , 6 , 2 , 2 ))
1172+ self .addCleanup (self ._delete_panels , p1 , p2 , p3 )
1173+
1174+ # The most recently created panel is on top.
1175+ self .assertIs (panel .top_panel (), p3 )
1176+ # window() returns the wrapped window.
1177+ self .assertEqual (p2 .window ().getbegyx (), (1 , 1 ))
1178+
1179+ # above()/below() walk the stack one step at a time.
1180+ self .assertIs (p1 .above (), p2 )
1181+ self .assertIs (p2 .above (), p3 )
1182+ self .assertIsNone (p3 .above ()) # nothing above the top panel
1183+ self .assertIs (p3 .below (), p2 )
1184+ self .assertIs (p2 .below (), p1 )
1185+
1186+ # top() raises a panel to the top, bottom() lowers it to the bottom.
1187+ p1 .top ()
1188+ self .assertIs (panel .top_panel (), p1 )
1189+ self .assertIsNone (p1 .above ())
1190+ p1 .bottom ()
1191+ self .assertIs (panel .bottom_panel (), p1 )
1192+ self .assertIsNone (p1 .below ())
1193+
1194+ # update_panels() refreshes the virtual screen from the stack.
1195+ panel .update_panels ()
1196+
1197+ @requires_curses_func ('panel' )
1198+ def test_panel_hide_show (self ):
1199+ p = curses .panel .new_panel (curses .newwin (3 , 6 , 0 , 0 ))
1200+ self .addCleanup (self ._delete_panels , p )
1201+ self .assertIs (p .hidden (), False )
1202+ p .hide ()
1203+ self .assertIs (p .hidden (), True )
1204+ p .show ()
1205+ self .assertIs (p .hidden (), False )
1206+
1207+ @requires_curses_func ('panel' )
1208+ def test_panel_move (self ):
1209+ win = curses .newwin (3 , 6 , 1 , 2 )
1210+ p = curses .panel .new_panel (win )
1211+ self .addCleanup (self ._delete_panels , p )
1212+ self .assertEqual (win .getbegyx (), (1 , 2 ))
1213+ p .move (4 , 5 )
1214+ self .assertEqual (win .getbegyx (), (4 , 5 ))
1215+
1216+ @requires_curses_func ('panel' )
1217+ def test_panel_replace (self ):
1218+ win1 = curses .newwin (3 , 6 , 0 , 0 )
1219+ win2 = curses .newwin (4 , 8 , 1 , 1 )
1220+ p = curses .panel .new_panel (win1 )
1221+ self .addCleanup (self ._delete_panels , p )
1222+ self .assertIs (p .window (), win1 )
1223+ p .replace (win2 )
1224+ self .assertIs (p .window (), win2 )
1225+
1226+ @requires_curses_func ('panel' )
1227+ def test_panel_userptr (self ):
1228+ p = curses .panel .new_panel (curses .newwin (3 , 6 , 0 , 0 ))
1229+ self .addCleanup (self ._delete_panels , p )
1230+ obj = ['userptr' ]
1231+ p .set_userptr (obj )
1232+ self .assertIs (p .userptr (), obj )
1233+
1234+ def _delete_panels (self , * panels ):
1235+ # Drop the panels from the global stack so they do not leak into
1236+ # later tests that inspect top_panel()/bottom_panel().
1237+ for p in panels :
1238+ try :
1239+ p .bottom ()
1240+ except curses .panel .error :
1241+ pass
1242+ del panels
1243+ gc_collect ()
1244+
1245+ def _make_textbox (self , nlines , ncols , * , insert_mode = False , stripspaces = 1 ):
1246+ win = curses .newwin (nlines , ncols , 0 , 0 )
1247+ box = curses .textpad .Textbox (win , insert_mode = insert_mode )
1248+ box .stripspaces = stripspaces
1249+ return box , win
1250+
1251+ def _type (self , box , text ):
1252+ for ch in text :
1253+ box .do_command (ch if isinstance (ch , int ) else ord (ch ))
1254+
1255+ def test_textbox_gather (self ):
1256+ # Typed text is read back by gather(). With stripspaces on (the
1257+ # default) gather() keeps a single trailing blank on a line and
1258+ # drops trailing empty lines.
1259+ box , win = self ._make_textbox (3 , 10 )
1260+ self ._type (box , 'Hello' )
1261+ self .assertEqual (box .gather (), 'Hello \n ' )
1262+
1263+ def test_textbox_gather_multiline (self ):
1264+ box , win = self ._make_textbox (3 , 10 )
1265+ self ._type (box , 'ab' )
1266+ box .do_command (curses .ascii .NL ) # ^j -> start of next line
1267+ self ._type (box , 'cd' )
1268+ self .assertEqual (box .gather (), 'ab \n cd \n ' )
1269+
1270+ def test_textbox_stripspaces (self ):
1271+ box , win = self ._make_textbox (1 , 8 , stripspaces = 1 )
1272+ self ._type (box , 'hi' )
1273+ self .assertEqual (box .gather (), 'hi ' )
1274+
1275+ box , win = self ._make_textbox (1 , 8 , stripspaces = 0 )
1276+ self ._type (box , 'hi' )
1277+ self .assertEqual (box .gather (), 'hi ' )
1278+
1279+ def test_textbox_insert_mode (self ):
1280+ # In insert mode a typed character shifts the rest of the line right.
1281+ box , win = self ._make_textbox (1 , 10 , insert_mode = True )
1282+ self ._type (box , 'aXc' )
1283+ win .move (0 , 1 )
1284+ self ._type (box , 'b' )
1285+ self .assertEqual (box .gather (), 'abXc ' )
1286+
1287+ def test_textbox_movement (self ):
1288+ box , win = self ._make_textbox (3 , 10 )
1289+ self ._type (box , 'abc' )
1290+ box .do_command (curses .ascii .SOH ) # ^a -> left edge
1291+ self .assertEqual (win .getyx (), (0 , 0 ))
1292+ box .do_command (curses .ascii .ENQ ) # ^e -> end of line
1293+ self .assertEqual (win .getyx (), (0 , 3 ))
1294+
1295+ def test_textbox_kill_to_eol (self ):
1296+ box , win = self ._make_textbox (1 , 10 )
1297+ self ._type (box , 'abcdef' )
1298+ win .move (0 , 3 )
1299+ box .do_command (curses .ascii .VT ) # ^k -> clear to end of line
1300+ self .assertEqual (box .gather (), 'abc ' )
1301+
1302+ def test_textbox_backspace (self ):
1303+ box , win = self ._make_textbox (1 , 10 )
1304+ self ._type (box , 'abc' )
1305+ box .do_command (curses .ascii .BS ) # ^h -> delete backward
1306+ self .assertEqual (box .gather (), 'ab ' )
1307+
1308+ def test_textbox_edit (self ):
1309+ # edit() reads characters until Ctrl-G and returns the contents.
1310+ box , win = self ._make_textbox (1 , 10 )
1311+ for ch in reversed ('Hi' + chr (curses .ascii .BEL )):
1312+ curses .ungetch (ch )
1313+ self .assertEqual (box .edit (), 'Hi ' )
1314+
1315+ def test_textbox_edit_validate (self ):
1316+ # The validate hook can rewrite an incoming keystroke.
1317+ box , win = self ._make_textbox (1 , 10 )
1318+ for ch in reversed ('abc' + chr (curses .ascii .BEL )):
1319+ curses .ungetch (ch )
1320+ box .edit (lambda ch : ord ('X' ) if ch == ord ('b' ) else ch )
1321+ self .assertEqual (box .gather (), 'aXc ' )
1322+
1323+ def test_textpad_rectangle (self ):
1324+ # rectangle() draws a box with ACS line/corner characters.
1325+ win = curses .newwin (6 , 12 , 0 , 0 )
1326+ curses .textpad .rectangle (win , 0 , 0 , 4 , 8 )
1327+ chartext = curses .A_CHARTEXT
1328+ self .assertEqual (win .inch (0 , 0 ) & chartext ,
1329+ curses .ACS_ULCORNER & chartext )
1330+ self .assertEqual (win .inch (0 , 8 ) & chartext ,
1331+ curses .ACS_URCORNER & chartext )
1332+ self .assertEqual (win .inch (4 , 0 ) & chartext ,
1333+ curses .ACS_LLCORNER & chartext )
1334+ self .assertEqual (win .inch (4 , 8 ) & chartext ,
1335+ curses .ACS_LRCORNER & chartext )
1336+ self .assertEqual (win .inch (0 , 1 ) & chartext ,
1337+ curses .ACS_HLINE & chartext )
1338+ self .assertEqual (win .inch (1 , 0 ) & chartext ,
1339+ curses .ACS_VLINE & chartext )
1340+
1341+ def test_wrapper (self ):
1342+ # wrapper() sets up curses, passes the screen to the callable along
1343+ # with extra arguments, returns its result and restores the terminal.
1344+ if not self .isatty :
1345+ self .skipTest ('requires terminal' )
1346+
1347+ def body (stdscr , a , b ):
1348+ self .assertIsInstance (stdscr , type (self .stdscr ))
1349+ self .assertIs (curses .isendwin (), False )
1350+ return a + b
1351+
1352+ self .assertEqual (curses .wrapper (body , 2 , 3 ), 5 )
1353+ self .assertIs (curses .isendwin (), True )
1354+ # wrapper() left the screen ended; revive it so the per-test
1355+ # endwin() cleanup does not fail with ERR.
1356+ curses .doupdate ()
1357+
10831358 @requires_curses_func ('is_term_resized' )
10841359 def test_is_term_resized (self ):
10851360 lines , cols = curses .LINES , curses .COLS
0 commit comments