feat: Add gallery presets, MultiDisplay support, and viewport tests

- Add ~20 gallery presets covering sources, effects, cameras, displays
- Add MultiDisplay support with --display multi:terminal,pygame syntax
- Fix ViewportFilterStage to recompute layout on viewport_width change
- Add benchmark.py module for hook-based performance testing
- Add viewport resize tests to test_viewport_filter_performance.py
This commit is contained in:
2026-03-17 01:24:15 -07:00
parent 57de835ae0
commit 05d261273e
8 changed files with 453 additions and 128 deletions

View File

@@ -30,11 +30,11 @@ class TestMain:
patch("engine.app.run_pipeline_mode") as mock_run,
):
mock_config.PIPELINE_DIAGRAM = False
mock_config.PRESET = "border-test"
mock_config.PRESET = "gallery-sources"
mock_config.PIPELINE_MODE = False
sys.argv = ["mainline.py"]
main()
mock_run.assert_called_once_with("border-test")
mock_run.assert_called_once_with("gallery-sources")
def test_main_exits_on_unknown_preset(self):
"""main() exits with error for unknown preset."""
@@ -120,11 +120,11 @@ class TestRunPipelineMode:
mock_create.return_value = mock_display
try:
run_pipeline_mode("border-test")
run_pipeline_mode("gallery-display-terminal")
except (KeyboardInterrupt, SystemExit):
pass
# Verify display was created with 'terminal' (preset display for border-test)
# Verify display was created with 'terminal' (preset display)
mock_create.assert_called_once_with("terminal")
def test_run_pipeline_mode_respects_display_cli_flag(self):

View File

@@ -158,3 +158,96 @@ class TestViewportFilterIntegration:
# Verify we kept the first N items in order
for i, item in enumerate(filtered):
assert item.content == f"Headline {i}"
class TestViewportResize:
"""Test ViewportFilterStage handles viewport resize correctly."""
def test_layout_recomputes_on_width_change(self):
"""Test that layout is recomputed when viewport_width changes."""
stage = ViewportFilterStage()
# Use long headlines that will wrap differently at different widths
items = [
SourceItem(
f"This is a very long headline number {i} that will definitely wrap at narrow widths",
"test",
str(i),
)
for i in range(50)
]
# Initial render at 80 cols
ctx = PipelineContext()
ctx.params = MockParams(viewport_width=80, viewport_height=24)
ctx.set("camera_y", 0)
stage.process(items, ctx)
cached_layout_80 = stage._layout.copy()
# Resize to 40 cols - layout should recompute
ctx.params.viewport_width = 40
stage.process(items, ctx)
cached_layout_40 = stage._layout.copy()
# With narrower viewport, items wrap to more lines
# So the cumulative heights should be different
assert cached_layout_40 != cached_layout_80, (
"Layout should recompute when viewport_width changes"
)
def test_layout_recomputes_on_height_change(self):
"""Test that visible items change when viewport_height changes."""
stage = ViewportFilterStage()
items = [SourceItem(f"Headline {i}", "test", str(i)) for i in range(100)]
ctx = PipelineContext()
ctx.set("camera_y", 0)
# Small viewport - fewer items visible
ctx.params = MockParams(viewport_width=80, viewport_height=12)
result_small = stage.process(items, ctx)
# Larger viewport - more items visible
ctx.params.viewport_height = 48
result_large = stage.process(items, ctx)
# With larger viewport, more items should be visible
assert len(result_large) >= len(result_small)
def test_camera_y_propagates_to_filter(self):
"""Test that camera_y is read from context."""
stage = ViewportFilterStage()
items = [SourceItem(f"Headline {i}", "test", str(i)) for i in range(100)]
ctx = PipelineContext()
ctx.params = MockParams(viewport_width=80, viewport_height=24)
# Camera at y=0
ctx.set("camera_y", 0)
result_at_0 = stage.process(items, ctx)
# Camera at y=100
ctx.set("camera_y", 100)
result_at_100 = stage.process(items, ctx)
# With different camera positions, different items should be visible
# (unless items are very short)
first_item_at_0 = result_at_0[0].content if result_at_0 else None
first_item_at_100 = result_at_100[0].content if result_at_100 else None
# The items at different positions should be different
assert first_item_at_0 != first_item_at_100 or first_item_at_0 is None
def test_resize_handles_edge_case_small_width(self):
"""Test that very narrow viewport doesn't crash."""
stage = ViewportFilterStage()
items = [SourceItem("Short", "test", "1")]
ctx = PipelineContext()
ctx.params = MockParams(viewport_width=10, viewport_height=5)
ctx.set("camera_y", 0)
# Should not crash with very narrow viewport
result = stage.process(items, ctx)
assert result is not None
assert len(result) > 0