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