blob: 4bb3069d3da94fa443c6d89dddf638a612c996f8 [file] [edit]
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (C) 2024 by the Linux Foundation
#
"""Headless Textual tests for review-TUI modal screens.
Uses Textual's built-in ``App.run_test()`` / ``Pilot`` harness so the
tests run without a real terminal. Only lightweight, self-contained
modals are exercised here — no database, network, or git needed.
"""
from typing import Any, Dict, List, Optional, Tuple
import pytest
from textual.app import App, ComposeResult
from textual.widgets import Input, Label, ListView
from b4.review_tui._modals import (
TRACKING_HELP_LINES,
ActionScreen,
ConfirmScreen,
HelpScreen,
LimitScreen,
NoteScreen,
PriorReviewScreen,
RevisionChoiceScreen,
SetStateScreen,
SnoozeScreen,
TrailerScreen,
UpdateRevisionScreen,
)
# ---------------------------------------------------------------------------
# Compat helper — Textual ≥ 1.0 (pip) uses Static.content,
# older builds (e.g. Fedora 43 package) still use Static.renderable.
# ---------------------------------------------------------------------------
def _static_text(widget: Any) -> str:
"""Return the text content of a Static widget across Textual versions."""
if hasattr(widget, 'content'):
return str(widget.content)
return str(widget.renderable)
# ---------------------------------------------------------------------------
# Minimal host app — just enough to push modal screens onto
# ---------------------------------------------------------------------------
class ModalTestApp(App[None]):
"""Bare app that serves as a host for pushing modal screens."""
def compose(self) -> ComposeResult:
yield Label('host')
# ---------------------------------------------------------------------------
# HelpScreen
# ---------------------------------------------------------------------------
class TestHelpScreen:
"""Tests for the HelpScreen modal."""
@staticmethod
def _lines() -> List[str]:
return [
'[bold]Test Help[/bold]\n',
'\n',
' [bold]a[/bold] Do alpha\n',
' [bold]b[/bold] Do bravo\n',
]
@pytest.mark.asyncio
async def test_escape_dismisses(self) -> None:
app = ModalTestApp()
dismissed: List[Any] = []
async with app.run_test() as pilot:
app.push_screen(HelpScreen(self._lines()), dismissed.append)
await pilot.pause()
# The help screen should now be on top
assert isinstance(app.screen, HelpScreen)
await pilot.press('escape')
await pilot.pause()
# Should be back on the host screen
assert not isinstance(app.screen, HelpScreen)
# https://github.com/python/mypy/issues/9457:
# app.screen is stale-narrowed across await.
assert dismissed == [None] # type: ignore[unreachable]
@pytest.mark.asyncio
async def test_question_mark_dismisses(self) -> None:
app = ModalTestApp()
dismissed: List[Any] = []
async with app.run_test() as pilot:
app.push_screen(HelpScreen(self._lines()), dismissed.append)
await pilot.pause()
await pilot.press('question_mark')
await pilot.pause()
assert not isinstance(app.screen, HelpScreen)
assert dismissed == [None]
@pytest.mark.asyncio
async def test_q_dismisses(self) -> None:
app = ModalTestApp()
dismissed: List[Any] = []
async with app.run_test() as pilot:
app.push_screen(HelpScreen(self._lines()), dismissed.append)
await pilot.pause()
await pilot.press('q')
await pilot.pause()
assert not isinstance(app.screen, HelpScreen)
assert dismissed == [None]
@pytest.mark.asyncio
async def test_scroll_bindings_do_not_dismiss(self) -> None:
"""j, k, space, etc. should scroll but keep the modal open."""
app = ModalTestApp()
async with app.run_test() as pilot:
app.push_screen(HelpScreen(TRACKING_HELP_LINES))
await pilot.pause()
assert isinstance(app.screen, HelpScreen)
for key in (
'j',
'k',
'down',
'up',
'space',
'backspace',
'pagedown',
'pageup',
):
await pilot.press(key)
await pilot.pause()
assert isinstance(app.screen, HelpScreen), (
f'{key!r} unexpectedly closed the help screen'
)
@pytest.mark.asyncio
async def test_content_rendered(self) -> None:
"""The static content inside the modal should contain our text."""
app = ModalTestApp()
async with app.run_test() as pilot:
lines = self._lines()
app.push_screen(HelpScreen(lines))
await pilot.pause()
dialog = app.screen.query_one('#help-dialog')
assert dialog is not None
# ---------------------------------------------------------------------------
# ConfirmScreen
# ---------------------------------------------------------------------------
class TestConfirmScreen:
"""Tests for the ConfirmScreen modal."""
@pytest.mark.asyncio
async def test_y_confirms(self) -> None:
app = ModalTestApp()
results: List[Optional[bool]] = []
async with app.run_test() as pilot:
app.push_screen(
ConfirmScreen('Delete?', ['This is permanent.']),
results.append,
)
await pilot.pause()
assert isinstance(app.screen, ConfirmScreen)
await pilot.press('y')
await pilot.pause()
assert not isinstance(app.screen, ConfirmScreen)
# https://github.com/python/mypy/issues/9457:
# app.screen is stale-narrowed across await.
assert results == [True] # type: ignore[unreachable]
@pytest.mark.asyncio
async def test_escape_cancels(self) -> None:
app = ModalTestApp()
results: List[Optional[bool]] = []
async with app.run_test() as pilot:
app.push_screen(
ConfirmScreen('Delete?', ['This is permanent.']),
results.append,
)
await pilot.pause()
await pilot.press('escape')
await pilot.pause()
assert not isinstance(app.screen, ConfirmScreen)
assert results == [False]
@pytest.mark.asyncio
async def test_body_lines_rendered(self) -> None:
"""Each body string should appear as a Static widget."""
app = ModalTestApp()
body = ['Line one.', 'Line two.', 'Line three.']
async with app.run_test() as pilot:
app.push_screen(ConfirmScreen('Title', body))
await pilot.pause()
statics = app.screen.query('#confirm-dialog Static')
# body lines + hint line + possibly title
rendered = [_static_text(s) for s in statics]
for line in body:
assert any(line in r for r in rendered), (
f'{line!r} not found in rendered statics'
)
@pytest.mark.asyncio
async def test_subject_shown(self) -> None:
app = ModalTestApp()
async with app.run_test() as pilot:
app.push_screen(
ConfirmScreen('Title', ['body'], subject='My Subject'),
)
await pilot.pause()
title_widget = app.screen.query_one('#confirm-title')
assert 'My Subject' in _static_text(title_widget)
@pytest.mark.asyncio
async def test_warning_border(self) -> None:
app = ModalTestApp()
async with app.run_test() as pilot:
app.push_screen(
ConfirmScreen('Oops', ['careful'], border='$warning'),
)
await pilot.pause()
dialog = app.screen.query_one('#confirm-dialog')
assert dialog.has_class('--border-warning')
# ---------------------------------------------------------------------------
# TrailerScreen
# ---------------------------------------------------------------------------
class TestTrailerScreen:
"""Tests for the TrailerScreen modal."""
@pytest.mark.asyncio
async def test_cancel_returns_none(self) -> None:
app = ModalTestApp()
results: List[Optional[List[str]]] = []
async with app.run_test() as pilot:
app.push_screen(TrailerScreen([]), results.append)
await pilot.pause()
assert isinstance(app.screen, TrailerScreen)
await pilot.press('escape')
await pilot.pause()
assert results == [None]
@pytest.mark.asyncio
async def test_confirm_empty_returns_empty_list(self) -> None:
"""With nothing toggled, q/confirm returns an empty list."""
app = ModalTestApp()
results: List[Optional[List[str]]] = []
async with app.run_test() as pilot:
app.push_screen(TrailerScreen([]), results.append)
await pilot.pause()
await pilot.press('q')
await pilot.pause()
assert len(results) == 1
assert results[0] == []
@pytest.mark.asyncio
async def test_existing_trailers_pretoggled(self) -> None:
"""Trailers already present should be pre-selected."""
app = ModalTestApp()
results: List[Optional[List[str]]] = []
async with app.run_test() as pilot:
app.push_screen(
TrailerScreen(['Reviewed-by: Alice <a@b.com>']),
results.append,
)
await pilot.pause()
# Confirm immediately — should have Reviewed-by selected
await pilot.press('q')
await pilot.pause()
assert results[0] == ['Reviewed-by']
@pytest.mark.asyncio
async def test_toggle_and_confirm(self) -> None:
"""Space toggles the highlighted item; q confirms."""
app = ModalTestApp()
results: List[Optional[List[str]]] = []
async with app.run_test() as pilot:
app.push_screen(TrailerScreen([]), results.append)
await pilot.pause()
# First item is Acked-by (index 0), toggle it
await pilot.press('space')
await pilot.pause()
# Move down to Reviewed-by, toggle it
await pilot.press('j')
await pilot.press('space')
await pilot.pause()
# Confirm
await pilot.press('q')
await pilot.pause()
assert len(results) == 1
assert results[0] is not None
assert 'Acked-by' in results[0]
assert 'Reviewed-by' in results[0]
assert 'Tested-by' not in results[0]
assert 'NACKed-by' not in results[0]
@pytest.mark.asyncio
async def test_toggle_twice_deselects(self) -> None:
"""Toggling the same item twice should deselect it."""
app = ModalTestApp()
results: List[Optional[List[str]]] = []
async with app.run_test() as pilot:
app.push_screen(TrailerScreen([]), results.append)
await pilot.pause()
await pilot.press('space') # select Acked-by
await pilot.press('space') # deselect Acked-by
await pilot.pause()
await pilot.press('q')
await pilot.pause()
assert results[0] == []
@pytest.mark.asyncio
async def test_jk_navigation(self) -> None:
"""j/k should move the highlight without toggling."""
app = ModalTestApp()
results: List[Optional[List[str]]] = []
async with app.run_test() as pilot:
app.push_screen(TrailerScreen([]), results.append)
await pilot.pause()
# Navigate down to Tested-by (index 2) and toggle only it
await pilot.press('j')
await pilot.press('j')
await pilot.press('space')
await pilot.pause()
await pilot.press('q')
await pilot.pause()
assert results[0] == ['Tested-by']
@pytest.mark.asyncio
async def test_enter_confirms(self) -> None:
"""Enter on the ListView should also confirm (via on_list_view_selected)."""
app = ModalTestApp()
results: List[Optional[List[str]]] = []
async with app.run_test() as pilot:
app.push_screen(
TrailerScreen(['Acked-by: Bob <b@c.com>']),
results.append,
)
await pilot.pause()
await pilot.press('enter')
await pilot.pause()
assert len(results) == 1
assert results[0] == ['Acked-by']
# ---------------------------------------------------------------------------
# NoteScreen
# ---------------------------------------------------------------------------
class TestNoteScreen:
"""Tests for the NoteScreen modal."""
@staticmethod
def _entries() -> List[Tuple[str, str, str]]:
return [
('Alice <alice@example.com>', 'green', 'Looks good to me.'),
('Bob <bob@example.com>', 'blue', 'Needs a rebase.'),
]
@pytest.mark.asyncio
async def test_escape_returns_none(self) -> None:
app = ModalTestApp()
results: List[Optional[str]] = []
async with app.run_test() as pilot:
app.push_screen(NoteScreen(self._entries()), results.append)
await pilot.pause()
assert isinstance(app.screen, NoteScreen)
await pilot.press('escape')
await pilot.pause()
assert results == [None]
@pytest.mark.asyncio
async def test_e_returns_edit(self) -> None:
app = ModalTestApp()
results: List[Optional[str]] = []
async with app.run_test() as pilot:
app.push_screen(NoteScreen(self._entries()), results.append)
await pilot.pause()
await pilot.press('e')
await pilot.pause()
assert results == ['__EDIT__']
@pytest.mark.asyncio
async def test_d_returns_delete(self) -> None:
app = ModalTestApp()
results: List[Optional[str]] = []
async with app.run_test() as pilot:
app.push_screen(NoteScreen(self._entries()), results.append)
await pilot.pause()
await pilot.press('d')
await pilot.pause()
assert results == ['__DELETE__']
@pytest.mark.asyncio
async def test_empty_notes(self) -> None:
"""Modal should still render with no entries."""
app = ModalTestApp()
async with app.run_test() as pilot:
app.push_screen(NoteScreen([]))
await pilot.pause()
assert isinstance(app.screen, NoteScreen)
assert app.screen.query_one('#note-viewer') is not None
# ---------------------------------------------------------------------------
# PriorReviewScreen
# ---------------------------------------------------------------------------
class TestPriorReviewScreen:
"""Tests for the PriorReviewScreen modal."""
@pytest.mark.asyncio
async def test_escape_dismisses(self) -> None:
app = ModalTestApp()
results: List[Any] = []
async with app.run_test() as pilot:
app.push_screen(
PriorReviewScreen('== Patch 1 ==\nLGTM'),
results.append,
)
await pilot.pause()
assert isinstance(app.screen, PriorReviewScreen)
await pilot.press('escape')
await pilot.pause()
assert not isinstance(app.screen, PriorReviewScreen)
# https://github.com/python/mypy/issues/9457:
# app.screen is stale-narrowed across await.
assert results == [None] # type: ignore[unreachable]
@pytest.mark.asyncio
async def test_content_rendered(self) -> None:
app = ModalTestApp()
async with app.run_test() as pilot:
app.push_screen(PriorReviewScreen('== Header ==\nSome text'))
await pilot.pause()
assert app.screen.query_one('#prior-review-viewer') is not None
# ---------------------------------------------------------------------------
# RevisionChoiceScreen
# ---------------------------------------------------------------------------
class TestRevisionChoiceScreen:
"""Tests for the RevisionChoiceScreen modal."""
@pytest.mark.asyncio
async def test_n_returns_newest(self) -> None:
app = ModalTestApp()
results: List[Optional[int]] = []
async with app.run_test() as pilot:
app.push_screen(
RevisionChoiceScreen(current_rev=2, newest_rev=5),
results.append,
)
await pilot.pause()
assert isinstance(app.screen, RevisionChoiceScreen)
await pilot.press('n')
await pilot.pause()
assert results == [5]
@pytest.mark.asyncio
async def test_o_returns_current(self) -> None:
app = ModalTestApp()
results: List[Optional[int]] = []
async with app.run_test() as pilot:
app.push_screen(
RevisionChoiceScreen(current_rev=2, newest_rev=5),
results.append,
)
await pilot.pause()
await pilot.press('o')
await pilot.pause()
assert results == [2]
@pytest.mark.asyncio
async def test_escape_returns_none(self) -> None:
app = ModalTestApp()
results: List[Optional[int]] = []
async with app.run_test() as pilot:
app.push_screen(
RevisionChoiceScreen(current_rev=1, newest_rev=3),
results.append,
)
await pilot.pause()
await pilot.press('escape')
await pilot.pause()
assert results == [None]
# ---------------------------------------------------------------------------
# SnoozeScreen
# ---------------------------------------------------------------------------
class TestSnoozeScreen:
"""Tests for the SnoozeScreen modal."""
@pytest.mark.asyncio
async def test_escape_cancels(self) -> None:
app = ModalTestApp()
results: List[Optional[Dict[str, str]]] = []
async with app.run_test() as pilot:
app.push_screen(SnoozeScreen(), results.append)
await pilot.pause()
assert isinstance(app.screen, SnoozeScreen)
await pilot.press('escape')
await pilot.pause()
assert results == [None]
@pytest.mark.asyncio
async def test_duration_snooze(self) -> None:
"""Entering a duration and confirming should return a datetime."""
app = ModalTestApp()
results: List[Optional[Dict[str, str]]] = []
async with app.run_test() as pilot:
app.push_screen(SnoozeScreen(), results.append)
await pilot.pause()
# The duration input should be focused by default
dur_input = app.screen.query_one('#snooze-duration', Input)
dur_input.value = '3d'
await pilot.press('ctrl+y')
await pilot.pause()
assert len(results) == 1
assert results[0] is not None
assert results[0]['source'] == 'duration'
assert results[0]['input'] == '3d'
# until should be an ISO datetime
assert 'T' in results[0]['until']
@pytest.mark.asyncio
async def test_tag_snooze(self) -> None:
app = ModalTestApp()
results: List[Optional[Dict[str, str]]] = []
async with app.run_test() as pilot:
app.push_screen(SnoozeScreen(), results.append)
await pilot.pause()
tag_input = app.screen.query_one('#snooze-tag', Input)
tag_input.value = 'v6.15-rc3'
await pilot.press('ctrl+y')
await pilot.pause()
assert len(results) == 1
assert results[0] is not None
assert results[0]['until'] == 'tag:v6.15-rc3'
assert results[0]['source'] == 'tag'
@pytest.mark.asyncio
async def test_empty_fields_shows_error(self) -> None:
"""Confirming with no fields filled should show an error, not dismiss."""
app = ModalTestApp()
results: List[Optional[Dict[str, str]]] = []
async with app.run_test() as pilot:
app.push_screen(SnoozeScreen(), results.append)
await pilot.pause()
await pilot.press('ctrl+y')
await pilot.pause()
# Should still be on the snooze screen (not dismissed)
assert isinstance(app.screen, SnoozeScreen)
assert len(results) == 0
error = app.screen.query_one('#snooze-error')
error_text = _static_text(error).lower()
assert 'enter' in error_text or 'please' in error_text
@pytest.mark.asyncio
async def test_multiple_fields_shows_error(self) -> None:
"""Filling more than one field should show an error."""
app = ModalTestApp()
results: List[Optional[Dict[str, str]]] = []
async with app.run_test() as pilot:
app.push_screen(SnoozeScreen(), results.append)
await pilot.pause()
app.screen.query_one('#snooze-duration', Input).value = '1d'
app.screen.query_one('#snooze-tag', Input).value = 'v6.15'
await pilot.press('ctrl+y')
await pilot.pause()
assert isinstance(app.screen, SnoozeScreen)
assert len(results) == 0
error = app.screen.query_one('#snooze-error')
assert 'only one' in _static_text(error).lower()
@pytest.mark.asyncio
async def test_invalid_duration_shows_error(self) -> None:
app = ModalTestApp()
results: List[Optional[Dict[str, str]]] = []
async with app.run_test() as pilot:
app.push_screen(SnoozeScreen(), results.append)
await pilot.pause()
app.screen.query_one('#snooze-duration', Input).value = 'banana'
await pilot.press('ctrl+y')
await pilot.pause()
assert isinstance(app.screen, SnoozeScreen)
assert len(results) == 0
@pytest.mark.asyncio
async def test_prepopulate_last_source(self) -> None:
"""Re-opening with last_source/last_input should pre-fill the field."""
app = ModalTestApp()
async with app.run_test() as pilot:
app.push_screen(SnoozeScreen(last_source='tag', last_input='v6.14'))
await pilot.pause()
tag_input = app.screen.query_one('#snooze-tag', Input)
assert tag_input.value == 'v6.14'
# ---------------------------------------------------------------------------
# SetStateScreen
# ---------------------------------------------------------------------------
class TestSetStateScreen:
"""Tests for the SetStateScreen modal."""
@staticmethod
def _states() -> List[Dict[str, Any]]:
return [
{'slug': 'new', 'name': 'New'},
{'slug': 'reviewing', 'name': 'Reviewing'},
{'slug': 'replied', 'name': 'Replied'},
{'slug': 'waiting', 'name': 'Waiting'},
]
@pytest.mark.asyncio
async def test_escape_cancels(self) -> None:
app = ModalTestApp()
results: List[Optional[Tuple[str, bool]]] = []
async with app.run_test() as pilot:
app.push_screen(
SetStateScreen(self._states(), 'new'),
results.append,
)
await pilot.pause()
assert isinstance(app.screen, SetStateScreen)
await pilot.press('escape')
await pilot.pause()
assert results == [None]
@pytest.mark.asyncio
async def test_select_state_with_enter(self) -> None:
app = ModalTestApp()
results: List[Optional[Tuple[str, bool]]] = []
async with app.run_test() as pilot:
app.push_screen(
SetStateScreen(self._states(), 'new'),
results.append,
)
await pilot.pause()
# Navigate to 'reviewing' (one down from 'new')
await pilot.press('j')
await pilot.press('enter')
await pilot.pause()
assert len(results) == 1
assert results[0] is not None
assert results[0][0] == 'reviewing'
assert results[0][1] is False # not archived
@pytest.mark.asyncio
async def test_archive_toggle(self) -> None:
app = ModalTestApp()
results: List[Optional[Tuple[str, bool]]] = []
async with app.run_test() as pilot:
app.push_screen(
SetStateScreen(self._states(), 'new'),
results.append,
)
await pilot.pause()
# Toggle archive, then confirm
await pilot.press('a')
await pilot.press('enter')
await pilot.pause()
assert len(results) == 1
assert results[0] is not None
assert results[0][1] is True # archived
@pytest.mark.asyncio
async def test_current_state_preselected(self) -> None:
"""The ListView index should start on the current state."""
app = ModalTestApp()
async with app.run_test() as pilot:
app.push_screen(SetStateScreen(self._states(), 'replied'))
await pilot.pause()
lv = app.screen.query_one('#state-list', ListView)
assert lv.index == 2 # 'replied' is at index 2
# ---------------------------------------------------------------------------
# LimitScreen
# ---------------------------------------------------------------------------
class TestLimitScreen:
"""Tests for the LimitScreen modal."""
@pytest.mark.asyncio
async def test_escape_cancels(self) -> None:
app = ModalTestApp()
results: List[Optional[str]] = []
async with app.run_test() as pilot:
app.push_screen(LimitScreen(), results.append)
await pilot.pause()
assert isinstance(app.screen, LimitScreen)
await pilot.press('escape')
await pilot.pause()
assert results == [None]
@pytest.mark.asyncio
async def test_enter_submits_value(self) -> None:
app = ModalTestApp()
results: List[Optional[str]] = []
async with app.run_test() as pilot:
app.push_screen(LimitScreen(), results.append)
await pilot.pause()
inp = app.screen.query_one('#limit-input', Input)
inp.value = 'netfilter'
await pilot.press('enter')
await pilot.pause()
assert results == ['netfilter']
@pytest.mark.asyncio
async def test_empty_enter_clears_filter(self) -> None:
"""Submitting empty input should return empty string (clear filter)."""
app = ModalTestApp()
results: List[Optional[str]] = []
async with app.run_test() as pilot:
app.push_screen(LimitScreen(), results.append)
await pilot.pause()
await pilot.press('enter')
await pilot.pause()
assert results == ['']
@pytest.mark.asyncio
async def test_current_pattern_prepopulated(self) -> None:
app = ModalTestApp()
async with app.run_test() as pilot:
app.push_screen(LimitScreen(current_pattern='drm'))
await pilot.pause()
inp = app.screen.query_one('#limit-input', Input)
assert inp.value == 'drm'
# ---------------------------------------------------------------------------
# ActionScreen
# ---------------------------------------------------------------------------
class TestActionScreen:
"""Tests for the ActionScreen modal."""
_SHORTCUTS = {
'review': 'r',
'take': 'T',
'snooze': 's',
'archive': 'x',
}
@staticmethod
def _actions() -> List[Tuple[str, str]]:
return [
('review', 'Review this series'),
('take', 'Take (apply) this series'),
('snooze', 'Snooze this series'),
('archive', 'Archive this series'),
]
@pytest.mark.asyncio
async def test_escape_cancels(self) -> None:
app = ModalTestApp()
results: List[Optional[str]] = []
async with app.run_test() as pilot:
app.push_screen(ActionScreen(self._actions()), results.append)
await pilot.pause()
assert isinstance(app.screen, ActionScreen)
await pilot.press('escape')
await pilot.pause()
assert results == [None]
@pytest.mark.asyncio
async def test_enter_confirms_highlighted(self) -> None:
app = ModalTestApp()
results: List[Optional[str]] = []
async with app.run_test() as pilot:
app.push_screen(ActionScreen(self._actions()), results.append)
await pilot.pause()
# First item is 'review'
await pilot.press('enter')
await pilot.pause()
assert results == ['review']
@pytest.mark.asyncio
async def test_navigate_and_confirm(self) -> None:
app = ModalTestApp()
results: List[Optional[str]] = []
async with app.run_test() as pilot:
app.push_screen(ActionScreen(self._actions()), results.append)
await pilot.pause()
await pilot.press('j') # take
await pilot.press('j') # snooze
await pilot.press('enter')
await pilot.pause()
assert results == ['snooze']
@pytest.mark.asyncio
async def test_shortcut_key_selects_directly(self) -> None:
"""Pressing the shortcut char should immediately dismiss."""
app = ModalTestApp()
results: List[Optional[str]] = []
async with app.run_test() as pilot:
app.push_screen(
ActionScreen(self._actions(), shortcuts=self._SHORTCUTS), results.append
)
await pilot.pause()
# 'T' is the shortcut for 'take'
await pilot.press('T')
await pilot.pause()
assert results == ['take']
@pytest.mark.asyncio
async def test_shortcut_r_for_review(self) -> None:
app = ModalTestApp()
results: List[Optional[str]] = []
async with app.run_test() as pilot:
app.push_screen(
ActionScreen(self._actions(), shortcuts=self._SHORTCUTS), results.append
)
await pilot.pause()
await pilot.press('r')
await pilot.pause()
assert results == ['review']
@pytest.mark.asyncio
async def test_shortcut_x_for_archive(self) -> None:
app = ModalTestApp()
results: List[Optional[str]] = []
async with app.run_test() as pilot:
app.push_screen(
ActionScreen(self._actions(), shortcuts=self._SHORTCUTS), results.append
)
await pilot.pause()
await pilot.press('x')
await pilot.pause()
assert results == ['archive']
# ---------------------------------------------------------------------------
# UpdateRevisionScreen
# ---------------------------------------------------------------------------
class TestUpdateRevisionScreen:
"""Tests for the UpdateRevisionScreen modal."""
@staticmethod
def _revisions() -> List[Dict[str, Any]]:
return [
{'revision': 2, 'subject': '[PATCH v2] fix the thing'},
{'revision': 3, 'subject': '[PATCH v3] fix the thing properly'},
{'revision': 4, 'subject': '[PATCH v4] fix the thing for real'},
]
@pytest.mark.asyncio
async def test_escape_cancels(self) -> None:
app = ModalTestApp()
results: List[Optional[int]] = []
async with app.run_test() as pilot:
app.push_screen(
UpdateRevisionScreen(1, self._revisions()),
results.append,
)
await pilot.pause()
assert isinstance(app.screen, UpdateRevisionScreen)
await pilot.press('escape')
await pilot.pause()
assert results == [None]
@pytest.mark.asyncio
async def test_select_first_revision(self) -> None:
app = ModalTestApp()
results: List[Optional[int]] = []
async with app.run_test() as pilot:
app.push_screen(
UpdateRevisionScreen(1, self._revisions()),
results.append,
)
await pilot.pause()
await pilot.press('enter')
await pilot.pause()
assert results == [2]
@pytest.mark.asyncio
async def test_navigate_and_select(self) -> None:
app = ModalTestApp()
results: List[Optional[int]] = []
async with app.run_test() as pilot:
app.push_screen(
UpdateRevisionScreen(1, self._revisions()),
results.append,
)
await pilot.pause()
await pilot.press('j') # v3
await pilot.press('j') # v4
await pilot.press('enter')
await pilot.pause()
assert results == [4]
@pytest.mark.asyncio
async def test_filters_older_revisions(self) -> None:
"""Only revisions newer than current should appear."""
app = ModalTestApp()
results: List[Optional[int]] = []
async with app.run_test() as pilot:
# current_revision=3, so only v4 should appear
app.push_screen(
UpdateRevisionScreen(3, self._revisions()),
results.append,
)
await pilot.pause()
# Only one item in the list — entering should select v4
await pilot.press('enter')
await pilot.pause()
assert results == [4]