commit d0b8f05c221e4005a61602481395fba1d424525e
parent beff97f84382cbcf3494ab1fd87b7b076ee0a2ca
Author: Dan Callaghan <djc@djc.id.au>
Date: Sat, 21 Mar 2026 17:13:20 +1100
tests: add coverage for lazy loading behaviour in LineCursorMode
Diffstat:
3 files changed, 197 insertions(+), 0 deletions(-)
diff --git a/Manifest.txt b/Manifest.txt
@@ -126,6 +126,7 @@ lib/sup/util/query.rb
lib/sup/util/uri.rb
lib/sup/version.rb
sup.gemspec
+test/dummy_buffer.rb
test/dummy_source.rb
test/fixtures/bad-content-transfer-encoding-1.eml
test/fixtures/binary-content-transfer-encoding-2.eml
@@ -174,6 +175,7 @@ test/unit/service/test_label_service.rb
test/unit/test_contact.rb
test/unit/test_edit_message_mode.rb
test/unit/test_horizontal_selector.rb
+test/unit/test_line_cursor_mode.rb
test/unit/test_locale_fiddler.rb
test/unit/test_person.rb
test/unit/test_rmail_message.rb
diff --git a/test/dummy_buffer.rb b/test/dummy_buffer.rb
@@ -0,0 +1,34 @@
+require "sup"
+
+module Redwood
+
+class DummyBuffer
+ attr_reader :width, :height, :dirty_count, :commit_count
+
+ def initialize width=80, height=25
+ @width = width
+ @height = height
+ @dirty = false
+ @dirty_count = 0
+ @commit_count = 0
+ end
+
+ def content_height; @height - 1; end
+ def content_width; @width; end
+
+ def mark_dirty
+ @dirty = true
+ @dirty_count += 1
+ end
+
+ def dirty?; @dirty; end
+
+ def commit
+ @dirty = false
+ @commit_count += 1
+ end
+
+ def write(*args); end
+end
+
+end
diff --git a/test/unit/test_line_cursor_mode.rb b/test/unit/test_line_cursor_mode.rb
@@ -0,0 +1,161 @@
+require "test_helper"
+require "dummy_buffer"
+
+require "sup"
+
+class TestLineCursorMode < Minitest::Test
+ def setup
+ $config = {
+ :load_more_threads_when_scrolling => true,
+ :continuous_scroll => false,
+ }
+ Redwood::BufferManager.init
+ @modes_to_cleanup = []
+ @lines = (0...40).map { |i| "line #{i}" }
+ @load_more = Thread::Queue.new
+ end
+
+ def teardown
+ @modes_to_cleanup.each { |mode| mode.cleanup }
+ Redwood::BufferManager.deinstantiate!
+ $config = nil
+ end
+
+ def make_mode
+ mode = Redwood::LineCursorMode.new
+ @modes_to_cleanup << mode
+ lines = @lines
+ mode.define_singleton_method(:lines) { lines.length }
+ mode.define_singleton_method(:[]) { |i| lines[i] }
+ mode.send(:to_load_more) { |n| @load_more << n }
+ mode.buffer = Redwood::DummyBuffer.new 100, lines.length + 1
+ mode.draw
+ mode
+ end
+
+ def expect_load_more n
+ begin
+ requested = @load_more.pop :timeout => 0.1
+ rescue ThreadError
+ ## Ruby < 3.2 does not obey the timeout for Queue#pop
+ sleep 0.1
+ requested = @load_more.pop true
+ end
+ refute_nil requested, "Expected load_more callbacks to fire"
+ assert_equal n, requested
+ (0...n).map { |i| @lines << "more line #{i}" }
+ end
+
+ def test_cursor_down
+ mode = make_mode
+
+ 20.times do
+ mode.handle_input Ncurses::CharCode.character('j')
+ end
+ assert_equal 20, mode.curpos
+ assert_equal 0, mode.topline
+
+ ## One past halfway, load_more callbacks are triggered.
+ mode.handle_input Ncurses::CharCode.character('j')
+ assert_equal 21, mode.curpos
+ expect_load_more 40
+
+ 18.times do
+ mode.handle_input Ncurses::CharCode.character('j')
+ end
+ assert_equal 39, mode.curpos
+ assert_equal 0, mode.topline
+
+ ## From the bottom line, it wraps back to the top of the next page.
+ mode.handle_input Ncurses::CharCode.character('j')
+ assert_equal 40, mode.curpos
+ assert_equal 40, mode.topline
+ end
+
+ def test_scroll_down
+ mode = make_mode
+
+ ## When the cursor is already at the top, it moves with the scroll.
+ assert_equal 0, mode.curpos
+ assert_equal 0, mode.topline
+ mode.handle_input Ncurses::CharCode.character('J')
+ assert_equal 1, mode.curpos
+ assert_equal 1, mode.topline
+
+ ## It always loads 10 more if we would scroll past the bottom.
+ expect_load_more 10
+
+ 3.times do
+ mode.handle_input Ncurses::CharCode.character('j')
+ end
+ assert_equal 4, mode.curpos
+ assert_equal 1, mode.topline
+
+ ## When the cursor is not at the top, it keeps its place and the
+ ## buffer scrolls underneath it.
+ mode.handle_input Ncurses::CharCode.character('J')
+ assert_equal 4, mode.curpos
+ assert_equal 2, mode.topline
+ end
+
+ def test_page_down
+ mode = make_mode
+
+ ## In theory, we should scroll a full page down. But because we always load
+ ## exactly enough lines to fill one page, the first time we are off by one.
+ mode.handle_input Ncurses::CharCode.keycode(Ncurses::KEY_NPAGE)
+ assert_equal 0, mode.curpos
+ assert_equal 39, mode.topline
+ expect_load_more 40
+
+ ## ThreadIndexMode#update does this, which I think is the only reason curpos
+ ## does not end up pointing to an invisible line...
+ mode.send :set_cursor_pos, 39
+ assert_equal 39, mode.curpos
+ assert_equal 39, mode.topline
+
+ ## Now we have 80 lines but the top is at line 39, which makes the next
+ ## page down behaviour even more awkward...
+ assert_equal 80, mode.lines
+ mode.handle_input Ncurses::CharCode.keycode(Ncurses::KEY_NPAGE)
+ assert_equal 79, mode.curpos
+ assert_equal 79, mode.topline
+ assert_equal 80, mode.lines
+ assert @load_more.empty?
+
+ ## Page down does not trigger load_more callbacks, which is not very nice.
+ mode.handle_input Ncurses::CharCode.keycode(Ncurses::KEY_NPAGE)
+ mode.handle_input Ncurses::CharCode.keycode(Ncurses::KEY_NPAGE)
+ mode.handle_input Ncurses::CharCode.keycode(Ncurses::KEY_NPAGE)
+ assert_equal 80, mode.lines
+ assert @load_more.empty?
+
+ ## Sending cursor_down to ThreadIndexView when it's in this state *does*
+ ## trigger load_more callbacks though. It doesn't happen in this test so
+ ## it's probably a side effect of some interactions between the two classes
+ ## like the #update method.
+ mode.handle_input Ncurses::CharCode.character('j')
+ #expect_load_more 40
+ assert_equal 79, mode.curpos
+ assert_equal 79, mode.topline
+ end
+
+ def test_page_down_when_fully_populated
+ mode = make_mode
+ (0...120).map { |i| @lines << "more line #{i}" } # enough for 4 full pages
+
+ mode.handle_input Ncurses::CharCode.keycode(Ncurses::KEY_NPAGE)
+ assert_equal 40, mode.curpos
+ assert_equal 40, mode.topline
+
+ ## Relative cursor position is preserved when paging down.
+ 3.times do
+ mode.handle_input Ncurses::CharCode.character('j')
+ end
+ assert_equal 43, mode.curpos
+ assert_equal 40, mode.topline
+ mode.handle_input Ncurses::CharCode.keycode(Ncurses::KEY_NPAGE)
+ assert_equal 83, mode.curpos
+ assert_equal 80, mode.topline
+ end
+end