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