split up the tests

This commit is contained in:
Anthony Sottile
2019-12-14 13:31:08 -08:00
parent d826cfbea1
commit 230e457e79
22 changed files with 2086 additions and 2032 deletions

0
testing/__init__.py Normal file
View File

142
testing/runner.py Normal file
View File

@@ -0,0 +1,142 @@
import contextlib
import shlex
import sys
from typing import List
from hecate import Runner
import babi
class PrintsErrorRunner(Runner):
def __init__(self, *args, **kwargs):
self._screenshots: List[str] = []
super().__init__(*args, **kwargs)
def screenshot(self, *args, **kwargs):
ret = super().screenshot(*args, **kwargs)
if not self._screenshots or self._screenshots[-1] != ret:
self._screenshots.append(ret)
return ret
@contextlib.contextmanager
def _onerror(self):
try:
yield
except Exception: # pragma: no cover
# take a screenshot of the final state
self.screenshot()
print('=' * 79, flush=True)
for screenshot in self._screenshots:
print(screenshot, end='', flush=True)
print('=' * 79, flush=True)
raise
def await_exit(self, *args, **kwargs):
with self._onerror():
return super().await_exit(*args, **kwargs)
def await_text(self, text, timeout=None):
"""copied from the base implementation but doesn't munge newlines"""
with self._onerror():
for _ in self.poll_until_timeout(timeout):
screen = self.screenshot()
if text in screen: # pragma: no branch
return
raise AssertionError(
f'Timeout while waiting for text {text!r} to appear',
)
def await_text_missing(self, s):
"""largely based on await_text"""
with self._onerror():
for _ in self.poll_until_timeout():
screen = self.screenshot()
munged = screen.replace('\n', '')
if s not in munged: # pragma: no branch
return
raise AssertionError(
f'Timeout while waiting for text {s!r} to disappear',
)
def get_pane_size(self):
cmd = ('display', '-t0', '-p', '#{pane_width}\t#{pane_height}')
w, h = self.tmux.execute_command(*cmd).split()
return int(w), int(h)
def _get_cursor_position(self):
cmd = ('display', '-t0', '-p', '#{cursor_x}\t#{cursor_y}')
x, y = self.tmux.execute_command(*cmd).split()
return int(x), int(y)
def await_cursor_position(self, *, x, y):
with self._onerror():
for _ in self.poll_until_timeout():
pos = self._get_cursor_position()
if pos == (x, y): # pragma: no branch
return
raise AssertionError(
f'Timeout while waiting for cursor to reach {(x, y)}\n'
f'Last cursor position: {pos}',
)
def get_screen_line(self, n):
return self.screenshot().splitlines()[n]
def get_cursor_line(self):
_, y = self._get_cursor_position()
return self.get_screen_line(y)
@contextlib.contextmanager
def resize(self, width, height):
current_w, current_h = self.get_pane_size()
sleep_cmd = (
'bash', '-c',
f'echo {"*" * (current_w * current_h)} && '
f'exec sleep infinity',
)
panes = 0
hsplit_w = current_w - width - 1
if hsplit_w > 0:
cmd = ('split-window', '-ht0', '-l', hsplit_w, *sleep_cmd)
self.tmux.execute_command(*cmd)
panes += 1
vsplit_h = current_h - height - 1
if vsplit_h > 0: # pragma: no branch # TODO
cmd = ('split-window', '-vt0', '-l', vsplit_h, *sleep_cmd)
self.tmux.execute_command(*cmd)
panes += 1
assert self.get_pane_size() == (width, height)
try:
yield
finally:
for _ in range(panes):
self.tmux.execute_command('kill-pane', '-t1')
def press_and_enter(self, s):
self.press(s)
self.press('Enter')
@contextlib.contextmanager
def run(*args, color=True, **kwargs):
cmd = (sys.executable, '-mcoverage', 'run', '-m', 'babi', *args)
quoted = ' '.join(shlex.quote(p) for p in cmd)
term = 'screen-256color' if color else 'screen'
cmd = ('bash', '-c', f'export TERM={term}; exec {quoted}')
with PrintsErrorRunner(*cmd, **kwargs) as h:
h.await_text(babi.VERSION_STR)
yield h
@contextlib.contextmanager
def and_exit(h):
yield
# only try and exit in non-exceptional cases
h.press('^X')
h.await_exit()

File diff suppressed because it is too large Load Diff

15
tests/color_test.py Normal file
View File

@@ -0,0 +1,15 @@
import pytest
from testing.runner import and_exit
from testing.runner import run
@pytest.mark.parametrize('color', (True, False))
def test_color_test(color):
with run('--color-test', color=color) as h, and_exit(h):
h.await_text('* 1* 2')
def test_can_start_without_color():
with run(color=False) as h, and_exit(h):
pass

145
tests/command_mode_test.py Normal file
View File

@@ -0,0 +1,145 @@
from testing.runner import and_exit
from testing.runner import run
def trigger_command_mode(h):
# in order to enter a steady state, trigger an unknown key first and then
# press escape to open the command mode. this is necessary as `Escape` is
# the start of "escape sequences" and sending characters too quickly will
# be interpreted as a single keypress
h.press('^J')
h.await_text('unknown key')
h.press('Escape')
h.await_text_missing('unknown key')
def test_quit_via_colon_q():
with run() as h:
trigger_command_mode(h)
h.press_and_enter(':q')
h.await_exit()
def test_key_navigation_in_command_mode():
with run() as h, and_exit(h):
trigger_command_mode(h)
h.press('hello world')
h.await_cursor_position(x=11, y=23)
h.press('Left')
h.await_cursor_position(x=10, y=23)
h.press('Right')
h.await_cursor_position(x=11, y=23)
h.press('Home')
h.await_cursor_position(x=0, y=23)
h.press('End')
h.await_cursor_position(x=11, y=23)
h.press('^A')
h.await_cursor_position(x=0, y=23)
h.press('^E')
h.await_cursor_position(x=11, y=23)
h.press('DC') # does nothing at end
h.await_cursor_position(x=11, y=23)
h.await_text('\nhello world\n')
h.press('Bspace')
h.await_cursor_position(x=10, y=23)
h.await_text('\nhello worl\n')
h.press('Home')
h.press('Bspace') # does nothing at beginning
h.await_cursor_position(x=0, y=23)
h.await_text('\nhello worl\n')
h.press('DC')
h.await_cursor_position(x=0, y=23)
h.await_text('\nello worl\n')
# unknown keys don't do anything
h.press('^J')
h.await_text('\nello worl\n')
h.press('Enter')
def test_save_via_command_mode(tmpdir):
f = tmpdir.join('f')
with run(str(f)) as h, and_exit(h):
h.press('hello world')
trigger_command_mode(h)
h.press_and_enter(':w')
assert f.read() == 'hello world\n'
def test_repeated_command_mode_does_not_show_previous_command(tmpdir):
f = tmpdir.join('f')
with run(str(f)) as h, and_exit(h):
h.press('ohai')
trigger_command_mode(h)
h.press_and_enter(':w')
trigger_command_mode(h)
h.await_text_missing(':w')
h.press('Enter')
def test_write_and_quit(tmpdir):
f = tmpdir.join('f')
with run(str(f)) as h, and_exit(h):
h.press('hello world')
trigger_command_mode(h)
h.press_and_enter(':wq')
h.await_exit()
assert f.read() == 'hello world\n'
def test_resizing_and_scrolling_in_command_mode():
with run(width=20) as h, and_exit(h):
h.press('a' * 15)
h.await_text(f'\n{"a" * 15}\n')
trigger_command_mode(h)
h.press('b' * 15)
h.await_text(f'\n{"b" * 15}\n')
with h.resize(width=16, height=24):
h.await_text('\n«aaaaaa\n') # the text contents
h.await_text('\n«bbbbbb\n') # the text contents
h.await_cursor_position(x=7, y=23)
h.press('Left')
h.await_cursor_position(x=14, y=23)
h.await_text(f'\n{"b" * 15}\n')
h.press('Enter')
def test_invalid_command():
with run() as h, and_exit(h):
trigger_command_mode(h)
h.press_and_enter(':fake')
h.await_text('invalid command: :fake')
def test_empty_command_is_noop():
with run() as h, and_exit(h):
h.press('hello ')
trigger_command_mode(h)
h.press('Enter')
h.press('world')
h.await_text('hello world')
h.await_text_missing('invalid command')
def test_cancel_command_mode():
with run() as h, and_exit(h):
h.press('hello ')
trigger_command_mode(h)
h.press(':q')
h.press('^C')
h.press('world')
h.await_text('hello world')
h.await_text_missing('invalid command')

18
tests/conftest.py Normal file
View File

@@ -0,0 +1,18 @@
import os
from unittest import mock
import pytest
@pytest.fixture(autouse=True)
def xdg_data_home(tmpdir):
data_home = tmpdir.join('data_home')
with mock.patch.dict(os.environ, {'XDG_DATA_HOME': str(data_home)}):
yield data_home
@pytest.fixture
def ten_lines(tmpdir):
f = tmpdir.join('f')
f.write('\n'.join(f'line_{i}' for i in range(10)))
yield f

View File

@@ -0,0 +1,19 @@
from testing.runner import and_exit
from testing.runner import run
def test_current_position(ten_lines):
with run(str(ten_lines)) as h, and_exit(h):
h.press('^C')
h.await_text('line 1, col 1 (of 10 lines)')
h.press('Right')
h.press('^C')
h.await_text('line 1, col 2 (of 10 lines)')
h.press('Down')
h.press('^C')
h.await_text('line 2, col 2 (of 10 lines)')
h.press('Up')
for i in range(10):
h.press('^K')
h.press('^C')
h.await_text('line 1, col 1 (of 1 line)')

49
tests/cut_uncut_test.py Normal file
View File

@@ -0,0 +1,49 @@
from testing.runner import and_exit
from testing.runner import run
def test_cut_and_uncut(ten_lines):
with run(str(ten_lines)) as h, and_exit(h):
h.press('^K')
h.await_text_missing('line_0')
h.await_text(' *')
h.press('^U')
h.await_text('line_0')
h.press('^Home')
h.press('^K')
h.press('^K')
h.await_text_missing('line_1')
h.press('^U')
h.await_text('line_0')
def test_cut_at_beginning_of_file():
with run() as h, and_exit(h):
h.press('^K')
h.press('^K')
h.press('^K')
h.await_text_missing('*')
def test_cut_end_of_file():
with run() as h, and_exit(h):
h.press('hi')
h.press('Down')
h.press('^K')
h.press('hi')
def test_cut_uncut_multiple_file_buffers(tmpdir):
f1 = tmpdir.join('f1')
f1.write('hello\nworld\n')
f2 = tmpdir.join('f2')
f2.write('good\nbye\n')
with run(str(f1), str(f2)) as h, and_exit(h):
h.press('^K')
h.await_text_missing('hello')
h.press('^X')
h.await_text_missing('world')
h.press('^U')
h.await_text('hello\ngood\nbye\n')

20
tests/file_test.py Normal file
View File

@@ -0,0 +1,20 @@
import babi
def test_position_repr():
ret = repr(babi.File('f.txt'))
assert ret == (
'File(\n'
" filename='f.txt',\n"
' modified=False,\n'
' lines=[],\n'
" nl='\\n',\n"
' file_y=0,\n'
' cursor_y=0,\n'
' x=0,\n'
' x_hint=0,\n'
' sha256=None,\n'
' undo_stack=[],\n'
' redo_stack=[],\n'
')'
)

27
tests/get_files_test.py Normal file
View File

@@ -0,0 +1,27 @@
import io
import pytest
import babi
@pytest.mark.parametrize(
('s', 'lines', 'nl', 'mixed'),
(
pytest.param('', [''], '\n', False, id='trivial'),
pytest.param('1\n2\n', ['1', '2', ''], '\n', False, id='lf'),
pytest.param('1\r\n2\r\n', ['1', '2', ''], '\r\n', False, id='crlf'),
pytest.param('1\r\n2\n', ['1', '2', ''], '\n', True, id='mixed'),
pytest.param('1\n2', ['1', '2', ''], '\n', False, id='noeol'),
),
)
def test_get_lines(s, lines, nl, mixed):
# sha256 tested below
ret_lines, ret_nl, ret_mixed, _ = babi._get_lines(io.StringIO(s))
assert (ret_lines, ret_nl, ret_mixed) == (lines, nl, mixed)
def test_get_lines_sha256_checksum():
ret = babi._get_lines(io.StringIO(''))
sha256 = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
assert ret == ([''], '\n', False, sha256)

69
tests/go_to_line_test.py Normal file
View File

@@ -0,0 +1,69 @@
import pytest
from testing.runner import and_exit
from testing.runner import run
def test_prompt_window_width():
with run() as h, and_exit(h):
h.press('^_')
h.await_text('enter line number:')
h.press('123')
with h.resize(width=23, height=24):
h.await_text('\nenter line number: «3')
with h.resize(width=22, height=24):
h.await_text('\nenter line numb…: «3')
with h.resize(width=7, height=24):
h.await_text('\n…: «3')
with h.resize(width=6, height=24):
h.await_text('\n123')
h.press('Enter')
def test_go_to_line_line(ten_lines):
def _jump_to_line(n):
h.press('^_')
h.await_text('enter line number:')
h.press_and_enter(str(n))
h.await_text_missing('enter line number:')
with run(str(ten_lines), height=9) as h, and_exit(h):
# still on screen
_jump_to_line(3)
h.await_cursor_position(x=0, y=3)
# should go to beginning of file
_jump_to_line(0)
h.await_cursor_position(x=0, y=1)
# should go to end of the file
_jump_to_line(999)
h.await_cursor_position(x=0, y=4)
assert h.get_screen_line(3) == 'line_9'
# should also go to the end of the file
_jump_to_line(-1)
h.await_cursor_position(x=0, y=4)
assert h.get_screen_line(3) == 'line_9'
# should go to beginning of file
_jump_to_line(-999)
h.await_cursor_position(x=0, y=1)
assert h.get_cursor_line() == 'line_0'
@pytest.mark.parametrize('key', ('Enter', '^C'))
def test_go_to_line_cancel(ten_lines, key):
with run(str(ten_lines)) as h, and_exit(h):
h.press('Down')
h.await_cursor_position(x=0, y=2)
h.press('^_')
h.await_text('enter line number:')
h.press(key)
h.await_cursor_position(x=0, y=2)
h.await_text('cancelled')
def test_go_to_line_not_an_integer():
with run() as h, and_exit(h):
h.press('^_')
h.await_text('enter line number:')
h.press_and_enter('asdf')
h.await_text("not an integer: 'asdf'")

144
tests/list_spy_test.py Normal file
View File

@@ -0,0 +1,144 @@
import pytest
import babi
def test_list_spy_repr():
assert repr(babi.ListSpy(['a', 'b', 'c'])) == "ListSpy(['a', 'b', 'c'])"
def test_list_spy_item_retrieval():
spy = babi.ListSpy(['a', 'b', 'c'])
assert spy[1] == 'b'
assert spy[-1] == 'c'
with pytest.raises(IndexError):
spy[3]
def test_list_spy_del():
lst = ['a', 'b', 'c']
spy = babi.ListSpy(lst)
del spy[1]
assert lst == ['a', 'c']
spy.undo(lst)
assert lst == ['a', 'b', 'c']
def test_list_spy_del_with_negative():
lst = ['a', 'b', 'c']
spy = babi.ListSpy(lst)
del spy[-1]
assert lst == ['a', 'b']
spy.undo(lst)
assert lst == ['a', 'b', 'c']
def test_list_spy_insert():
lst = ['a', 'b', 'c']
spy = babi.ListSpy(lst)
spy.insert(1, 'q')
assert lst == ['a', 'q', 'b', 'c']
spy.undo(lst)
assert lst == ['a', 'b', 'c']
def test_list_spy_insert_with_negative():
lst = ['a', 'b', 'c']
spy = babi.ListSpy(lst)
spy.insert(-1, 'q')
assert lst == ['a', 'b', 'q', 'c']
spy.undo(lst)
assert lst == ['a', 'b', 'c']
def test_list_spy_set_value():
lst = ['a', 'b', 'c']
spy = babi.ListSpy(lst)
spy[1] = 'hello'
assert lst == ['a', 'hello', 'c']
spy.undo(lst)
assert lst == ['a', 'b', 'c']
def test_list_spy_multiple_modifications():
lst = ['a', 'b', 'c']
spy = babi.ListSpy(lst)
spy[1] = 'hello'
spy.insert(1, 'ohai')
del spy[0]
assert lst == ['ohai', 'hello', 'c']
spy.undo(lst)
assert lst == ['a', 'b', 'c']
def test_list_spy_iter():
spy = babi.ListSpy(['a', 'b', 'c'])
spy_iter = iter(spy)
assert next(spy_iter) == 'a'
assert next(spy_iter) == 'b'
assert next(spy_iter) == 'c'
with pytest.raises(StopIteration):
next(spy_iter)
def test_list_spy_append():
lst = ['a', 'b', 'c']
spy = babi.ListSpy(lst)
spy.append('q')
assert lst == ['a', 'b', 'c', 'q']
spy.undo(lst)
assert lst == ['a', 'b', 'c']
def test_list_spy_pop_default():
lst = ['a', 'b', 'c']
spy = babi.ListSpy(lst)
spy.pop()
assert lst == ['a', 'b']
spy.undo(lst)
assert lst == ['a', 'b', 'c']
def test_list_spy_pop_idx():
lst = ['a', 'b', 'c']
spy = babi.ListSpy(lst)
spy.pop(1)
assert lst == ['a', 'c']
spy.undo(lst)
assert lst == ['a', 'b', 'c']

269
tests/movement_test.py Normal file
View File

@@ -0,0 +1,269 @@
import pytest
from testing.runner import and_exit
from testing.runner import run
def test_arrow_key_movement(tmpdir):
f = tmpdir.join('f')
f.write(
'short\n'
'\n'
'long long long long\n',
)
with run(str(f)) as h, and_exit(h):
h.await_text('short')
h.await_cursor_position(x=0, y=1)
# should not go off the beginning of the file
h.press('Left')
h.await_cursor_position(x=0, y=1)
h.press('Up')
h.await_cursor_position(x=0, y=1)
# left and right should work
h.press('Right')
h.press('Right')
h.await_cursor_position(x=2, y=1)
h.press('Left')
h.await_cursor_position(x=1, y=1)
# up should still be a noop on line 1
h.press('Up')
h.await_cursor_position(x=1, y=1)
# down once should put it on the beginning of the second line
h.press('Down')
h.await_cursor_position(x=0, y=2)
# down again should restore the x positon on the next line
h.press('Down')
h.await_cursor_position(x=1, y=3)
# down once more should put it on the special end-of-file line
h.press('Down')
h.await_cursor_position(x=0, y=4)
# should not go off the end of the file
h.press('Down')
h.await_cursor_position(x=0, y=4)
h.press('Right')
h.await_cursor_position(x=0, y=4)
# left should put it at the end of the line
h.press('Left')
h.await_cursor_position(x=19, y=3)
# right should put it to the next line
h.press('Right')
h.await_cursor_position(x=0, y=4)
# if the hint-x is too high it should not go past the end of line
h.press('Left')
h.press('Up')
h.press('Up')
h.await_cursor_position(x=5, y=1)
# and moving back down should still retain the hint-x
h.press('Down')
h.press('Down')
h.await_cursor_position(x=19, y=3)
@pytest.mark.parametrize(
('page_up', 'page_down'),
(('PageUp', 'PageDown'), ('^Y', '^V')),
)
def test_page_up_and_page_down(ten_lines, page_up, page_down):
with run(str(ten_lines), height=10) as h, and_exit(h):
h.press('Down')
h.press('Down')
h.press(page_up)
h.await_cursor_position(x=0, y=1)
h.press(page_down)
h.await_text('line_8')
h.await_cursor_position(x=0, y=1)
assert h.get_cursor_line() == 'line_6'
h.press(page_up)
h.await_text_missing('line_8')
h.await_cursor_position(x=0, y=1)
assert h.get_cursor_line() == 'line_0'
h.press(page_down)
h.press(page_down)
h.await_cursor_position(x=0, y=5)
assert h.get_cursor_line() == ''
h.press('Up')
h.await_cursor_position(x=0, y=4)
assert h.get_cursor_line() == 'line_9'
def test_page_up_page_down_size_small_window(ten_lines):
with run(str(ten_lines), height=4) as h, and_exit(h):
h.press('PageDown')
h.await_text('line_2')
h.await_cursor_position(x=0, y=1)
assert h.get_cursor_line() == 'line_1'
h.press('Down')
h.press('PageUp')
h.await_text_missing('line_2')
h.await_cursor_position(x=0, y=1)
assert h.get_cursor_line() == 'line_0'
def test_ctrl_home(ten_lines):
with run(str(ten_lines), height=4) as h, and_exit(h):
for _ in range(3):
h.press('PageDown')
h.await_text_missing('line_0')
h.press('^Home')
h.await_text('line_0')
h.await_cursor_position(x=0, y=1)
def test_ctrl_end(ten_lines):
with run(str(ten_lines), height=6) as h, and_exit(h):
h.press('^End')
h.await_cursor_position(x=0, y=3)
assert h.get_screen_line(2) == 'line_9'
def test_ctrl_end_already_on_last_page(ten_lines):
with run(str(ten_lines), height=9) as h, and_exit(h):
h.press('PageDown')
h.await_cursor_position(x=0, y=1)
h.await_text('line_9')
h.press('^End')
h.await_cursor_position(x=0, y=6)
assert h.get_screen_line(5) == 'line_9'
def test_scrolling_arrow_key_movement(ten_lines):
with run(str(ten_lines), height=10) as h, and_exit(h):
h.await_text('line_7')
# we should not have scrolled after 7 presses
for _ in range(7):
h.press('Down')
h.await_text('line_0')
h.await_cursor_position(x=0, y=8)
# but this should scroll down
h.press('Down')
h.await_text('line_8')
h.await_cursor_position(x=0, y=4)
assert h.get_cursor_line() == 'line_8'
# we should not have scrolled after 3 up presses
for _ in range(3):
h.press('Up')
h.await_text('line_9')
# but this should scroll up
h.press('Up')
h.await_text('line_0')
def test_ctrl_down_beginning_of_file(ten_lines):
with run(str(ten_lines), height=5) as h, and_exit(h):
h.press('^Down')
h.await_text('line_3')
h.await_cursor_position(x=0, y=1)
assert h.get_cursor_line() == 'line_1'
def test_ctrl_up_moves_screen_up_one_line(ten_lines):
with run(str(ten_lines), height=5) as h, and_exit(h):
h.press('^Down')
h.press('^Up')
h.await_text('line_0')
h.await_text('line_2')
h.await_cursor_position(x=0, y=2)
def test_ctrl_up_at_beginning_of_file_does_nothing(ten_lines):
with run(str(ten_lines), height=5) as h, and_exit(h):
h.press('^Up')
h.await_text('line_0')
h.await_text('line_2')
h.await_cursor_position(x=0, y=1)
def test_ctrl_up_at_bottom_of_screen(ten_lines):
with run(str(ten_lines), height=5) as h, and_exit(h):
h.press('^Down')
h.press('Down')
h.press('Down')
h.await_text('line_1')
h.await_text('line_3')
h.await_cursor_position(x=0, y=3)
h.press('^Up')
h.await_text('line_0')
h.await_cursor_position(x=0, y=3)
def test_ctrl_down_at_end_of_file(ten_lines):
with run(str(ten_lines), height=5) as h, and_exit(h):
h.press('^End')
for i in range(4):
h.press('^Down')
h.press('Up')
h.await_text('line_9')
assert h.get_cursor_line() == 'line_9'
def test_ctrl_down_causing_cursor_movement_should_fix_x(tmpdir):
f = tmpdir.join('f')
f.write('line_1\n\nline_2\n\nline_3\n\nline_4\n')
with run(str(f), height=5) as h, and_exit(h):
h.press('Right')
h.press('^Down')
h.await_text_missing('\nline_1\n')
h.await_cursor_position(x=0, y=1)
def test_ctrl_up_causing_cursor_movement_should_fix_x(tmpdir):
f = tmpdir.join('f')
f.write('line_1\n\nline_2\n\nline_3\n\nline_4\n')
with run(str(f), height=5) as h, and_exit(h):
h.press('^Down')
h.press('^Down')
h.press('Down')
h.press('Down')
h.press('Right')
h.await_text('line_3')
h.press('^Up')
h.await_text_missing('3')
h.await_cursor_position(x=0, y=3)
@pytest.mark.parametrize('k', ('End', '^E'))
def test_end_key(tmpdir, k):
f = tmpdir.join('f')
f.write('hello world\nhello world\n')
with run(str(f)) as h, and_exit(h):
h.await_text('hello world')
h.await_cursor_position(x=0, y=1)
h.press(k)
h.await_cursor_position(x=11, y=1)
h.press('Down')
h.await_cursor_position(x=11, y=2)
@pytest.mark.parametrize('k', ('Home', '^A'))
def test_home_key(tmpdir, k):
f = tmpdir.join('f')
f.write('hello world\nhello world\n')
with run(str(f)) as h, and_exit(h):
h.await_text('hello world')
h.press('Down')
h.press('Left')
h.await_cursor_position(x=11, y=1)
h.press(k)
h.await_cursor_position(x=0, y=1)
h.press('Down')
h.await_cursor_position(x=0, y=2)
def test_page_up_does_not_go_negative(ten_lines):
with run(str(ten_lines), height=10) as h, and_exit(h):
for _ in range(8):
h.press('Down')
h.await_cursor_position(x=0, y=4)
h.press('^Y')
h.await_cursor_position(x=0, y=1)
assert h.get_cursor_line() == 'line_0'

View File

@@ -0,0 +1,55 @@
from testing.runner import run
def test_multiple_files(tmpdir):
a = tmpdir.join('file_a')
a.write('a text')
b = tmpdir.join('file_b')
b.write('b text')
c = tmpdir.join('file_c')
c.write('c text')
with run(str(a), str(b), str(c)) as h:
h.await_text('file_a')
h.await_text('[1/3]')
h.await_text('a text')
h.press('Right')
h.await_cursor_position(x=1, y=1)
h.press('M-Right')
h.await_text('file_b')
h.await_text('[2/3]')
h.await_text('b text')
h.await_cursor_position(x=0, y=1)
h.press('M-Left')
h.await_text('file_a')
h.await_text('[1/3]')
h.await_cursor_position(x=1, y=1)
# wrap around
h.press('M-Left')
h.await_text('file_c')
h.await_text('[3/3]')
h.await_text('c text')
# make sure to clear statuses when switching files
h.press('^J')
h.await_text('unknown key')
h.press('M-Right')
h.await_text_missing('unknown key')
h.press('^J')
h.await_text('unknown key')
h.press('M-Left')
h.await_text_missing('unknown key')
# also make sure to clear statuses when exiting files
h.press('^J')
h.await_text('unknown key')
h.press('^X')
h.await_text('file_a')
h.await_text_missing('unknown key')
h.press('^X')
h.await_text('file_b')
h.press('^X')
h.await_exit()

235
tests/replace_test.py Normal file
View File

@@ -0,0 +1,235 @@
import pytest
from testing.runner import and_exit
from testing.runner import run
@pytest.mark.parametrize('key', ('^C', 'Enter'))
def test_replace_cancel(key):
with run() as h, and_exit(h):
h.press('^\\')
h.await_text('search (to replace):')
h.press(key)
h.await_text('cancelled')
def test_replace_invalid_regex():
with run() as h, and_exit(h):
h.press('^\\')
h.await_text('search (to replace):')
h.press_and_enter('(')
h.await_text("invalid regex: '('")
def test_replace_cancel_at_replace_string():
with run() as h, and_exit(h):
h.press('^\\')
h.await_text('search (to replace):')
h.press_and_enter('hello')
h.await_text('replace with:')
h.press('^C')
h.await_text('cancelled')
def test_replace_actual_contents(ten_lines):
with run(str(ten_lines)) as h, and_exit(h):
h.press('^\\')
h.await_text('search (to replace):')
h.press_and_enter('line_0')
h.await_text('replace with:')
h.press_and_enter('ohai')
h.await_text('replace [y(es), n(o), a(ll)]?')
h.press('y')
h.await_text_missing('line_0')
h.await_text('ohai')
h.await_text(' *')
h.await_text('replaced 1 occurrence')
def test_replace_cancel_at_individual_replace(ten_lines):
with run(str(ten_lines)) as h, and_exit(h):
h.press('^\\')
h.await_text('search (to replace):')
h.press_and_enter(r'line_\d')
h.await_text('replace with:')
h.press_and_enter('ohai')
h.await_text('replace [y(es), n(o), a(ll)]?')
h.press('^C')
h.await_text('cancelled')
def test_replace_unknown_characters_at_individual_replace(ten_lines):
with run(str(ten_lines)) as h, and_exit(h):
h.press('^\\')
h.await_text('search (to replace):')
h.press_and_enter(r'line_\d')
h.await_text('replace with:')
h.press_and_enter('ohai')
h.await_text('replace [y(es), n(o), a(ll)]?')
h.press('?')
h.press('^C')
h.await_text('cancelled')
def test_replace_say_no_to_individual_replace(ten_lines):
with run(str(ten_lines)) as h, and_exit(h):
h.press('^\\')
h.await_text('search (to replace):')
h.press_and_enter('line_[135]')
h.await_text('replace with:')
h.press_and_enter('ohai')
h.await_text('replace [y(es), n(o), a(ll)]?')
h.press('y')
h.await_text_missing('line_1')
h.press('n')
h.await_text('line_3')
h.press('y')
h.await_text_missing('line_5')
h.await_text('replaced 2 occurrences')
def test_replace_all(ten_lines):
with run(str(ten_lines)) as h, and_exit(h):
h.press('^\\')
h.await_text('search (to replace):')
h.press_and_enter(r'line_(\d)')
h.await_text('replace with:')
h.press_and_enter(r'ohai+\1')
h.await_text('replace [y(es), n(o), a(ll)]?')
h.press('a')
h.await_text_missing('line')
h.await_text('ohai+1')
h.await_text('replaced 10 occurrences')
def test_replace_with_empty_string(ten_lines):
with run(str(ten_lines)) as h, and_exit(h):
h.press('^\\')
h.await_text('search (to replace):')
h.press_and_enter('line_1')
h.await_text('replace with:')
h.press('Enter')
h.await_text('replace [y(es), n(o), a(ll)]?')
h.press('y')
h.await_text_missing('line_1')
def test_replace_search_not_found(ten_lines):
with run(str(ten_lines)) as h, and_exit(h):
h.press('^\\')
h.await_text('search (to replace):')
h.press_and_enter('wat')
# TODO: would be nice to not prompt for a replace string in this case
h.await_text('replace with:')
h.press('Enter')
h.await_text('no matches')
def test_replace_small_window_size(ten_lines):
with run(str(ten_lines)) as h, and_exit(h):
h.press('^\\')
h.await_text('search (to replace):')
h.press_and_enter('line')
h.await_text('replace with:')
h.press_and_enter('wat')
h.await_text('replace [y(es), n(o), a(ll)]?')
with h.resize(width=8, height=24):
h.await_text('replace…')
h.press('^C')
def test_replace_line_goes_off_screen():
with run() as h, and_exit(h):
h.press(f'{"a" * 20}{"b" * 90}')
h.press('^A')
h.await_text(f'{"a" * 20}{"b" * 59}»')
h.press('^\\')
h.await_text('search (to replace):')
h.press_and_enter('b+')
h.await_text('replace with:')
h.press_and_enter('wat')
h.await_text('replace [y(es), n(o), a(ll)]?')
h.await_text(f'{"a" * 20}{"b" * 59}»')
h.press('y')
h.await_text(f'{"a" * 20}wat')
h.await_text('replaced 1 occurrence')
def test_replace_undo_undoes_only_one(ten_lines):
with run(str(ten_lines)) as h, and_exit(h):
h.press('^\\')
h.await_text('search (to replace):')
h.press_and_enter('line')
h.await_text('replace with:')
h.press_and_enter('wat')
h.press('y')
h.await_text_missing('line_0')
h.press('y')
h.await_text_missing('line_1')
h.press('^C')
h.press('M-u')
h.await_text('line_1')
h.await_text_missing('line_0')
def test_replace_multiple_occurrences_in_line():
with run() as h, and_exit(h):
h.press('baaaaabaaaaa')
h.press('^\\')
h.await_text('search (to replace):')
h.press_and_enter('a+')
h.await_text('replace with:')
h.press_and_enter('q')
h.await_text('replace [y(es), n(o), a(ll)]?')
h.press('a')
h.await_text('bqbq')
def test_replace_after_wrapping(ten_lines):
with run(str(ten_lines)) as h, and_exit(h):
h.press('Down')
h.press('^\\')
h.await_text('search (to replace):')
h.press_and_enter('line_[02]')
h.await_text('replace with:')
h.press_and_enter('ohai')
h.await_text('replace [y(es), n(o), a(ll)]?')
h.press('y')
h.await_text_missing('line_2')
h.press('y')
h.await_text_missing('line_0')
h.await_text('replaced 2 occurrences')
def test_replace_after_cursor_after_wrapping():
with run() as h, and_exit(h):
h.press('baaab')
h.press('Left')
h.press('^\\')
h.await_text('search (to replace):')
h.press_and_enter('b')
h.await_text('replace with:')
h.press_and_enter('q')
h.await_text('replace [y(es), n(o), a(ll)]?')
h.press('n')
h.press('y')
h.await_text('replaced 1 occurrence')
h.await_text('qaaab')
def test_replace_separate_line_after_wrapping(ten_lines):
with run(str(ten_lines)) as h, and_exit(h):
h.press('Down')
h.press('Down')
h.press('^\\')
h.await_text('search (to replace):')
h.press_and_enter('line_[01]')
h.await_text('replace with:')
h.press_and_enter('_')
h.await_text('replace [y(es), n(o), a(ll)]?')
h.press('y')
h.await_text_missing('line_0')
h.press('y')
h.await_text_missing('line_1')

161
tests/resize_test.py Normal file
View File

@@ -0,0 +1,161 @@
import babi
from testing.runner import and_exit
from testing.runner import run
def test_window_height_2(tmpdir):
# 2 tall:
# - header is hidden, otherwise behaviour is normal
f = tmpdir.join('f.txt')
f.write('hello world')
with run(str(f)) as h, and_exit(h):
h.await_text('hello world')
with h.resize(80, 2):
h.await_text_missing(babi.VERSION_STR)
assert h.screenshot() == 'hello world\n\n'
h.press('^J')
h.await_text('unknown key')
h.await_text(babi.VERSION_STR)
def test_window_height_1(tmpdir):
# 1 tall:
# - only file contents as body
# - status takes precedence over body, but cleared after single action
f = tmpdir.join('f.txt')
f.write('hello world')
with run(str(f)) as h, and_exit(h):
h.await_text('hello world')
with h.resize(80, 1):
h.await_text_missing(babi.VERSION_STR)
assert h.screenshot() == 'hello world\n'
h.press('^J')
h.await_text('unknown key')
h.press('Right')
h.await_text_missing('unknown key')
h.press('Down')
def test_reacts_to_resize():
with run() as h, and_exit(h):
first_line = h.get_screen_line(0)
with h.resize(40, 20):
# the first line should be different after resize
h.await_text_missing(first_line)
def test_resize_scrolls_up(ten_lines):
with run(str(ten_lines)) as h, and_exit(h):
h.await_text('line_9')
for _ in range(7):
h.press('Down')
h.await_cursor_position(x=0, y=8)
# a resize to a height of 10 should not scroll
with h.resize(80, 10):
h.await_text_missing('line_8')
h.await_cursor_position(x=0, y=8)
h.await_text('line_8')
# but a resize to smaller should
with h.resize(80, 9):
h.await_text_missing('line_0')
h.await_cursor_position(x=0, y=3)
# make sure we're still on the same line
assert h.get_cursor_line() == 'line_7'
def test_resize_scroll_does_not_go_negative(ten_lines):
with run(str(ten_lines)) as h, and_exit(h):
for _ in range(5):
h.press('Down')
h.await_cursor_position(x=0, y=6)
with h.resize(80, 7):
h.await_text_missing('line_9')
h.await_text('line_9')
for _ in range(2):
h.press('Up')
assert h.get_screen_line(1) == 'line_0'
def test_horizontal_scrolling(tmpdir):
f = tmpdir.join('f')
lots_of_text = ''.join(
''.join(str(i) * 10 for i in range(10))
for _ in range(10)
)
f.write(f'line1\n{lots_of_text}\n')
with run(str(f)) as h, and_exit(h):
h.await_text('6777777777»')
h.press('Down')
for _ in range(78):
h.press('Right')
h.await_text('6777777777»')
h.press('Right')
h.await_text('«77777778')
h.await_text('344444444445»')
h.await_cursor_position(x=7, y=2)
for _ in range(71):
h.press('Right')
h.await_text('«77777778')
h.await_text('344444444445»')
h.press('Right')
h.await_text('«444445')
h.await_text('1222»')
def test_horizontal_scrolling_exact_width(tmpdir):
f = tmpdir.join('f')
f.write('0' * 80)
with run(str(f)) as h, and_exit(h):
h.await_text('000')
for _ in range(78):
h.press('Right')
h.await_text_missing('»')
h.await_cursor_position(x=78, y=1)
h.press('Right')
h.await_text('«0000000')
h.await_cursor_position(x=7, y=1)
def test_horizontal_scrolling_narrow_window(tmpdir):
f = tmpdir.join('f')
f.write(''.join(str(i) * 10 for i in range(10)))
with run(str(f)) as h, and_exit(h):
with h.resize(5, 24):
h.await_text('0000»')
for _ in range(3):
h.press('Right')
h.await_text('0000»')
h.press('Right')
h.await_cursor_position(x=3, y=1)
h.await_text('«000»')
for _ in range(6):
h.press('Right')
h.await_text('«001»')
def test_window_width_1(tmpdir):
f = tmpdir.join('f')
f.write('hello')
with run(str(f)) as h, and_exit(h):
with h.resize(1, 24):
h.await_text('»')
for _ in range(3):
h.press('Right')
h.await_text('hello')
h.await_cursor_position(x=3, y=1)

103
tests/save_test.py Normal file
View File

@@ -0,0 +1,103 @@
import pytest
from testing.runner import and_exit
from testing.runner import run
def test_mixed_newlines(tmpdir):
f = tmpdir.join('f')
f.write_binary(b'foo\nbar\r\n')
with run(str(f)) as h, and_exit(h):
# should start as modified
h.await_text('f *')
h.await_text(r"mixed newlines will be converted to '\n'")
def test_new_file():
with run('this_is_a_new_file') as h, and_exit(h):
h.await_text('this_is_a_new_file')
h.await_text('(new file)')
def test_not_a_file(tmpdir):
d = tmpdir.join('d').ensure_dir()
with run(str(d)) as h, and_exit(h):
h.await_text('<<new file>>')
h.await_text("d' is not a file")
def test_save_no_filename_specified(tmpdir):
f = tmpdir.join('f')
with run() as h, and_exit(h):
h.press('hello world')
h.press('^S')
h.await_text('enter filename:')
h.press_and_enter(str(f))
h.await_text('saved! (1 line written)')
h.await_text_missing('*')
assert f.read() == 'hello world\n'
@pytest.mark.parametrize('k', ('Enter', '^C'))
def test_save_no_filename_specified_cancel(k):
with run() as h, and_exit(h):
h.press('hello world')
h.press('^S')
h.await_text('enter filename:')
h.press(k)
h.await_text('cancelled')
def test_saving_file_on_disk_changes(tmpdir):
# TODO: this should show some sort of diffing thing or just allow overwrite
f = tmpdir.join('f')
with run(str(f)) as h, and_exit(h):
f.write('hello world')
h.press('^S')
h.await_text('file changed on disk, not implemented')
def test_allows_saving_same_contents_as_modified_contents(tmpdir):
f = tmpdir.join('f')
with run(str(f)) as h, and_exit(h):
f.write('hello world\n')
h.press('hello world')
h.await_text('hello world')
h.press('^S')
h.await_text('saved! (1 line written)')
h.await_text_missing('*')
assert f.read() == 'hello world\n'
def test_allows_saving_if_file_on_disk_does_not_change(tmpdir):
f = tmpdir.join('f')
f.write('hello world\n')
with run(str(f)) as h, and_exit(h):
h.await_text('hello world')
h.press('ohai')
h.press('Enter')
h.press('^S')
h.await_text('saved! (2 lines written)')
h.await_text_missing('*')
assert f.read() == 'ohai\nhello world\n'
def test_save_file_when_it_did_not_exist(tmpdir):
f = tmpdir.join('f')
with run(str(f)) as h, and_exit(h):
h.press('hello world')
h.press('^S')
h.await_text('saved! (1 line written)')
h.await_text_missing('*')
assert f.read() == 'hello world\n'

298
tests/search_test.py Normal file
View File

@@ -0,0 +1,298 @@
import pytest
from testing.runner import and_exit
from testing.runner import run
def test_search_wraps(ten_lines):
with run(str(ten_lines)) as h, and_exit(h):
h.press('Down')
h.press('Down')
h.await_cursor_position(x=0, y=3)
h.press('^W')
h.await_text('search:')
h.press_and_enter('^line_0$')
h.await_text('search wrapped')
h.await_cursor_position(x=0, y=1)
def test_search_find_next_line(ten_lines):
with run(str(ten_lines)) as h, and_exit(h):
h.await_cursor_position(x=0, y=1)
h.press('^W')
h.await_text('search:')
h.press_and_enter('^line_')
h.await_cursor_position(x=0, y=2)
def test_search_find_later_in_line():
with run() as h, and_exit(h):
h.press_and_enter('lol')
h.press('Up')
h.press('Right')
h.await_cursor_position(x=1, y=1)
h.press('^W')
h.await_text('search:')
h.press_and_enter('l')
h.await_cursor_position(x=2, y=1)
def test_search_only_one_match_already_at_that_match(ten_lines):
with run(str(ten_lines)) as h, and_exit(h):
h.press('Down')
h.await_cursor_position(x=0, y=2)
h.press('^W')
h.await_text('search:')
h.press_and_enter('^line_1$')
h.await_text('this is the only occurrence')
h.await_cursor_position(x=0, y=2)
def test_search_not_found(ten_lines):
with run(str(ten_lines)) as h, and_exit(h):
h.press('^W')
h.await_text('search:')
h.press_and_enter('this will not match')
h.await_text('no matches')
h.await_cursor_position(x=0, y=1)
def test_search_invalid_regex(ten_lines):
with run(str(ten_lines)) as h, and_exit(h):
h.press('^W')
h.await_text('search:')
h.press_and_enter('invalid(regex')
h.await_text("invalid regex: 'invalid(regex'")
@pytest.mark.parametrize('key', ('Enter', '^C'))
def test_search_cancel(ten_lines, key):
with run(str(ten_lines)) as h, and_exit(h):
h.press('^W')
h.await_text('search:')
h.press(key)
h.await_text('cancelled')
def test_search_repeated_search(ten_lines):
with run(str(ten_lines)) as h, and_exit(h):
h.press('^W')
h.press('line')
h.await_text('search: line')
h.press('Enter')
h.await_cursor_position(x=0, y=2)
h.press('^W')
h.await_text('search [line]:')
h.press('Enter')
h.await_cursor_position(x=0, y=3)
def test_search_history_recorded():
with run() as h, and_exit(h):
h.press('^W')
h.await_text('search:')
h.press_and_enter('asdf')
h.await_text('no matches')
h.press('^W')
h.press('Up')
h.await_text('search [asdf]: asdf')
h.press('BSpace')
h.press('test')
h.await_text('search [asdf]: asdtest')
h.press('Down')
h.await_text_missing('asdtest')
h.press('Down') # can't go past the end
h.press('Up')
h.await_text('asdtest')
h.press('Up') # can't go past the beginning
h.await_text('asdtest')
h.press('enter')
h.await_text('no matches')
h.press('^W')
h.press('Up')
h.await_text('search [asdtest]: asdtest')
h.press('Up')
h.await_text('search [asdtest]: asdf')
h.press('^C')
def test_search_history_duplicates_dont_repeat():
with run() as h, and_exit(h):
h.press('^W')
h.await_text('search:')
h.press_and_enter('search1')
h.await_text('no matches')
h.press('^W')
h.await_text('search [search1]:')
h.press_and_enter('search2')
h.await_text('no matches')
h.press('^W')
h.await_text('search [search2]:')
h.press_and_enter('search2')
h.await_text('no matches')
h.press('^W')
h.press('Up')
h.await_text('search2')
h.press('Up')
h.await_text('search1')
h.press('Enter')
def test_search_history_is_saved_between_sessions(xdg_data_home):
with run() as h, and_exit(h):
h.press('^W')
h.press_and_enter('search1')
h.press('^W')
h.press_and_enter('search2')
contents = xdg_data_home.join('babi/history/search').read()
assert contents == 'search1\nsearch2\n'
with run() as h, and_exit(h):
h.press('^W')
h.press('Up')
h.await_text('search: search2')
h.press('Up')
h.await_text('search: search1')
h.press('Enter')
def test_search_multiple_sessions_append_to_history(xdg_data_home):
xdg_data_home.join('babi/history/search').ensure().write(
'orig\n'
'history\n',
)
with run() as h1, and_exit(h1):
with run() as h2, and_exit(h2):
h2.press('^W')
h2.press_and_enter('h2 history')
h1.press('^W')
h1.press_and_enter('h1 history')
contents = xdg_data_home.join('babi/history/search').read()
assert contents == (
'orig\n'
'history\n'
'h2 history\n'
'h1 history\n'
)
def test_search_reverse_search_history(xdg_data_home, ten_lines):
xdg_data_home.join('babi/history/search').ensure().write(
'line_5\n'
'line_3\n'
'line_1\n',
)
with run(str(ten_lines)) as h, and_exit(h):
h.press('^W')
h.press('^R')
h.await_text('search(reverse-search)``:')
h.press('line')
h.await_text('search(reverse-search)`line`: line_1')
h.press('a')
h.await_text('search(failed reverse-search)`linea`: line_1')
h.press('BSpace')
h.await_text('search(reverse-search)`line`: line_1')
h.press('^R')
h.await_text('search(reverse-search)`line`: line_3')
h.press('Enter')
h.await_cursor_position(x=0, y=4)
def test_search_reverse_search_history_pos_after(xdg_data_home, ten_lines):
xdg_data_home.join('babi/history/search').ensure().write(
'line_3\n',
)
with run(str(ten_lines), height=20) as h, and_exit(h):
h.press('^W')
h.press('^R')
h.press('line')
h.await_text('search(reverse-search)`line`: line_3')
h.press('Right')
h.await_text('search: line_3')
h.await_cursor_position(y=19, x=14)
h.press('^C')
def test_search_reverse_search_enter_saves_entry(xdg_data_home, ten_lines):
xdg_data_home.join('babi/history/search').ensure().write(
'line_1\n'
'line_3\n',
)
with run(str(ten_lines)) as h, and_exit(h):
h.press('^W')
h.press('^R')
h.press('1')
h.await_text('search(reverse-search)`1`: line_1')
h.press('Enter')
h.press('^W')
h.press('Up')
h.await_text('search [line_1]: line_1')
h.press('^C')
def test_search_reverse_search_history_cancel():
with run() as h, and_exit(h):
h.press('^W')
h.press('^R')
h.await_text('search(reverse-search)``:')
h.press('^C')
h.await_text('cancelled')
def test_search_reverse_search_resizing():
with run() as h, and_exit(h):
h.press('^W')
h.press('^R')
with h.resize(width=24, height=24):
h.await_text('search(reverse-se…:')
h.press('^C')
def test_search_reverse_search_does_not_wrap_around(xdg_data_home):
xdg_data_home.join('babi/history/search').ensure().write(
'line_1\n'
'line_3\n',
)
with run() as h, and_exit(h):
h.press('^W')
h.press('^R')
# this should not wrap around
for i in range(6):
h.press('^R')
h.await_text('search(reverse-search)``: line_1')
h.press('^C')
def test_search_reverse_search_ctrl_r_on_failed_match(xdg_data_home):
xdg_data_home.join('babi/history/search').ensure().write(
'nomatch\n'
'line_1\n',
)
with run() as h, and_exit(h):
h.press('^W')
h.press('^R')
h.press('line')
h.await_text('search(reverse-search)`line`: line_1')
h.press('^R')
h.await_text('search(failed reverse-search)`line`: line_1')
h.press('^C')
def test_search_reverse_search_keeps_current_text_displayed():
with run() as h, and_exit(h):
h.press('^W')
h.press('ohai')
h.await_text('search: ohai')
h.press('^R')
h.await_text('search(reverse-search)``: ohai')
h.press('^C')

20
tests/status_test.py Normal file
View File

@@ -0,0 +1,20 @@
from testing.runner import and_exit
from testing.runner import run
def test_status_clearing_behaviour():
with run() as h, and_exit(h):
h.press('^J')
h.await_text('unknown key')
for i in range(24):
h.press('LEFT')
h.await_text('unknown key')
h.press('LEFT')
h.await_text_missing('unknown key')
def test_very_narrow_window_status():
with run(height=50) as h, and_exit(h):
with h.resize(5, 50):
h.press('^J')
h.await_text('unkno')

48
tests/suspend_test.py Normal file
View File

@@ -0,0 +1,48 @@
import shlex
import sys
import babi
from testing.runner import PrintsErrorRunner
def test_suspend(tmpdir):
f = tmpdir.join('f')
f.write('hello')
with PrintsErrorRunner('bash', '--norc') as h:
cmd = (sys.executable, '-mcoverage', 'run', '-m', 'babi', str(f))
h.press_and_enter(' '.join(shlex.quote(part) for part in cmd))
h.await_text(babi.VERSION_STR)
h.await_text('hello')
h.press('^Z')
h.await_text_missing('hello')
h.press_and_enter('fg')
h.await_text('hello')
h.press('^X')
h.press_and_enter('exit')
h.await_exit()
def test_suspend_with_resize(tmpdir):
f = tmpdir.join('f')
f.write('hello')
with PrintsErrorRunner('bash', '--norc') as h:
cmd = (sys.executable, '-mcoverage', 'run', '-m', 'babi', str(f))
h.press_and_enter(' '.join(shlex.quote(part) for part in cmd))
h.await_text(babi.VERSION_STR)
h.await_text('hello')
h.press('^Z')
h.await_text_missing('hello')
with h.resize(80, 10):
h.press_and_enter('fg')
h.await_text('hello')
h.press('^X')
h.press_and_enter('exit')
h.await_exit()

122
tests/text_editing_test.py Normal file
View File

@@ -0,0 +1,122 @@
from testing.runner import and_exit
from testing.runner import run
def test_basic_text_editing(tmpdir):
with run() as h, and_exit(h):
h.press('hello world')
h.await_text('hello world')
h.press('Down')
h.press('bye!')
h.await_text('bye!')
assert h.screenshot().strip().endswith('world\nbye!')
def test_backspace_at_beginning_of_file():
with run() as h, and_exit(h):
h.press('Bspace')
h.await_text_missing('unknown key')
assert h.screenshot().strip().splitlines()[1:] == []
assert '*' not in h.screenshot()
def test_backspace_joins_lines(tmpdir):
f = tmpdir.join('f')
f.write('foo\nbar\nbaz\n')
with run(str(f)) as h, and_exit(h):
h.await_text('foo')
h.press('Down')
h.press('Bspace')
h.await_text('foobar')
h.await_text('f *')
h.await_cursor_position(x=3, y=1)
# pressing down should retain the X position
h.press('Down')
h.await_cursor_position(x=3, y=2)
def test_backspace_at_end_of_file_still_allows_scrolling_down(tmpdir):
f = tmpdir.join('f')
f.write('hello world')
with run(str(f)) as h, and_exit(h):
h.await_text('hello world')
h.press('Down')
h.press('Bspace')
h.press('Down')
h.await_cursor_position(x=0, y=2)
assert '*' not in h.screenshot()
def test_backspace_deletes_text(tmpdir):
f = tmpdir.join('f')
f.write('ohai there')
with run(str(f)) as h, and_exit(h):
h.await_text('ohai there')
for _ in range(3):
h.press('Right')
h.press('Bspace')
h.await_text('ohi')
h.await_text('f *')
h.await_cursor_position(x=2, y=1)
def test_delete_at_end_of_file(tmpdir):
with run() as h, and_exit(h):
h.press('DC')
h.await_text_missing('unknown key')
h.await_text_missing('*')
def test_delete_removes_character_afterwards(tmpdir):
f = tmpdir.join('f')
f.write('hello world')
with run(str(f)) as h, and_exit(h):
h.await_text('hello world')
h.press('Right')
h.press('DC')
h.await_text('hllo world')
h.await_text('f *')
def test_delete_at_end_of_line(tmpdir):
f = tmpdir.join('f')
f.write('hello\nworld\n')
with run(str(f)) as h, and_exit(h):
h.await_text('hello')
h.press('Down')
h.press('Left')
h.press('DC')
h.await_text('helloworld')
h.await_text('f *')
def test_press_enter_beginning_of_file(tmpdir):
f = tmpdir.join('f')
f.write('hello world')
with run(str(f)) as h, and_exit(h):
h.await_text('hello world')
h.press('Enter')
h.await_text('\n\nhello world')
h.await_cursor_position(x=0, y=2)
h.await_text('f *')
def test_press_enter_mid_line(tmpdir):
f = tmpdir.join('f')
f.write('hello world')
with run(str(f)) as h, and_exit(h):
h.await_text('hello world')
for _ in range(5):
h.press('Right')
h.press('Enter')
h.await_text('hello\n world')
h.await_cursor_position(x=0, y=2)
h.press('Up')
h.await_cursor_position(x=0, y=1)

127
tests/undo_redo_test.py Normal file
View File

@@ -0,0 +1,127 @@
from testing.runner import and_exit
from testing.runner import run
def test_nothing_to_undo_redo():
with run() as h, and_exit(h):
h.press('M-u')
h.await_text('nothing to undo!')
h.press('M-U')
h.await_text('nothing to redo!')
def test_undo_redo():
with run() as h, and_exit(h):
h.press('hello')
h.await_text('hello')
h.press('M-u')
h.await_text('undo: text')
h.await_text_missing('hello')
h.await_text_missing(' *')
h.press('M-U')
h.await_text('redo: text')
h.await_text('hello')
h.await_text(' *')
def test_undo_redo_movement_interrupts_actions():
with run() as h, and_exit(h):
h.press('hello')
h.press('Left')
h.press('Right')
h.press('world')
h.press('M-u')
h.await_text('undo: text')
h.await_text('hello')
def test_undo_redo_action_interrupts_actions():
with run() as h, and_exit(h):
h.press('hello')
h.await_text('hello')
h.press('Bspace')
h.await_text_missing('hello')
h.press('M-u')
h.await_text('hello')
h.press('world')
h.await_text('helloworld')
h.press('M-u')
h.await_text_missing('world')
h.await_text('hello')
def test_undo_redo_mixed_newlines(tmpdir):
f = tmpdir.join('f')
f.write_binary(b'foo\nbar\r\n')
with run(str(f)) as h, and_exit(h):
h.press('hello')
h.press('M-u')
h.await_text('undo: text')
h.await_text(' *')
def test_undo_redo_with_save(tmpdir):
f = tmpdir.join('f').ensure()
with run(str(f)) as h, and_exit(h):
h.press('hello')
h.press('^S')
h.await_text_missing(' *')
h.press('M-u')
h.await_text(' *')
h.press('M-U')
h.await_text_missing(' *')
h.press('M-u')
h.await_text(' *')
h.press('^S')
h.await_text_missing(' *')
h.press('M-U')
h.await_text(' *')
def test_undo_redo_implicit_linebreak(tmpdir):
f = tmpdir.join('f')
with run(str(f)) as h, and_exit(h):
h.press('hello')
h.press('M-u')
h.press('^S')
h.await_text('saved!')
assert f.read() == ''
h.press('M-U')
h.press('^S')
h.await_text('saved!')
assert f.read() == 'hello\n'
def test_redo_cleared_after_action(tmpdir):
with run() as h, and_exit(h):
h.press('hello')
h.press('M-u')
h.press('world')
h.press('M-U')
h.await_text('nothing to redo!')
def test_undo_no_action_when_noop():
with run() as h, and_exit(h):
h.press('hello')
h.press('Enter')
h.press('world')
h.press('Down')
h.press('^K')
h.press('M-u')
h.await_text('undo: text')
h.await_cursor_position(x=0, y=2)
def test_undo_redo_causes_scroll():
with run(height=8) as h, and_exit(h):
for i in range(10):
h.press('Enter')
h.await_cursor_position(x=0, y=3)
h.press('M-u')
h.await_cursor_position(x=0, y=1)
h.press('M-U')
h.await_cursor_position(x=0, y=4)