""" Visual verification tests for message overlay and effect rendering. These tests verify that the sideline pipeline produces visual output that matches the expected behavior of upstream/main, even if the buffer format differs due to architectural differences. """ import json from pathlib import Path import pytest from engine.display import DisplayRegistry from engine.pipeline import Pipeline, PipelineConfig, PipelineContext from engine.pipeline.adapters import create_stage_from_display from engine.pipeline.params import PipelineParams from engine.pipeline.presets import get_preset class TestMessageOverlayVisuals: """Test message overlay visual rendering.""" def test_message_overlay_produces_output(self): """Verify message overlay stage produces output when ntfy message is present.""" # This test verifies the message overlay stage is working # It doesn't compare with upstream, just verifies functionality from engine.pipeline.adapters.message_overlay import MessageOverlayStage from engine.pipeline.adapters import MessageOverlayConfig # Test the rendering function directly stage = MessageOverlayStage( config=MessageOverlayConfig(enabled=True, display_secs=30) ) # Test with a mock message msg = ("Test Title", "Test Message Body", 0.0) w, h = 80, 24 # Render overlay overlay, _ = stage._render_message_overlay(msg, w, h, (None, None)) # Verify overlay has content assert len(overlay) > 0, "Overlay should have content when message is present" # Verify overlay contains expected content overlay_text = "".join(overlay) # Note: Message body is rendered as block characters, not text # The title appears in the metadata line assert "Test Title" in overlay_text, "Overlay should contain message title" assert "ntfy" in overlay_text, "Overlay should contain ntfy metadata" assert "\033[" in overlay_text, "Overlay should contain ANSI codes" def test_message_overlay_appears_in_correct_position(self): """Verify message overlay appears in centered position.""" # This test verifies the message overlay positioning logic # It checks that the overlay coordinates are calculated correctly from engine.pipeline.adapters.message_overlay import MessageOverlayStage from engine.pipeline.adapters import MessageOverlayConfig stage = MessageOverlayStage(config=MessageOverlayConfig()) # Test positioning calculation msg = ("Test Title", "Test Body", 0.0) w, h = 80, 24 # Render overlay overlay, _ = stage._render_message_overlay(msg, w, h, (None, None)) # Verify overlay has content assert len(overlay) > 0, "Overlay should have content" # Verify overlay contains cursor positioning codes overlay_text = "".join(overlay) assert "\033[" in overlay_text, "Overlay should contain ANSI codes" assert "H" in overlay_text, "Overlay should contain cursor positioning" # Verify panel is centered (check first line's position) # Panel height is len(msg_rows) + 2 (content + meta + border) # panel_top = max(0, (h - panel_h) // 2) # First content line should be at panel_top + 1 first_line = overlay[0] assert "\033[" in first_line, "First line should have cursor positioning" assert ";1H" in first_line, "First line should position at column 1" def test_theme_system_integration(self): """Verify theme system is integrated with message overlay.""" from engine import config as engine_config from engine.themes import THEME_REGISTRY # Verify theme registry has expected themes assert "green" in THEME_REGISTRY, "Green theme should exist" assert "orange" in THEME_REGISTRY, "Orange theme should exist" assert "purple" in THEME_REGISTRY, "Purple theme should exist" # Verify active theme is set assert engine_config.ACTIVE_THEME is not None, "Active theme should be set" assert engine_config.ACTIVE_THEME.name in THEME_REGISTRY, ( "Active theme should be in registry" ) # Verify theme has gradient colors assert len(engine_config.ACTIVE_THEME.main_gradient) == 12, ( "Main gradient should have 12 colors" ) assert len(engine_config.ACTIVE_THEME.message_gradient) == 12, ( "Message gradient should have 12 colors" ) class TestPipelineExecutionOrder: """Test pipeline execution order for visual consistency.""" def test_message_overlay_after_camera(self): """Verify message overlay is applied after camera transformation.""" from engine.pipeline import Pipeline, PipelineConfig, PipelineContext from engine.pipeline.adapters import ( create_stage_from_display, MessageOverlayStage, MessageOverlayConfig, ) from engine.display import DisplayRegistry # Create pipeline config = PipelineConfig( source="empty", display="null", camera="feed", effects=[], ) ctx = PipelineContext() pipeline = Pipeline(config=config, context=ctx) # Add stages from engine.data_sources.sources import EmptyDataSource from engine.pipeline.adapters import DataSourceStage pipeline.add_stage( "source", DataSourceStage(EmptyDataSource(width=80, height=24), name="empty"), ) pipeline.add_stage( "message_overlay", MessageOverlayStage(config=MessageOverlayConfig()) ) pipeline.add_stage( "display", create_stage_from_display(DisplayRegistry.create("null"), "null") ) # Build and check order pipeline.build() execution_order = pipeline.execution_order # Verify message_overlay comes after camera stages camera_idx = next( (i for i, name in enumerate(execution_order) if "camera" in name), -1 ) msg_idx = next( (i for i, name in enumerate(execution_order) if "message_overlay" in name), -1, ) if camera_idx >= 0 and msg_idx >= 0: assert msg_idx > camera_idx, "Message overlay should be after camera stage" class TestCapturedOutputAnalysis: """Test analysis of captured output files.""" def test_captured_files_exist(self): """Verify captured output files exist.""" sideline_path = Path("output/sideline_demo.json") upstream_path = Path("output/upstream_demo.json") assert sideline_path.exists(), "Sideline capture file should exist" assert upstream_path.exists(), "Upstream capture file should exist" def test_captured_files_valid(self): """Verify captured output files are valid JSON.""" sideline_path = Path("output/sideline_demo.json") upstream_path = Path("output/upstream_demo.json") with open(sideline_path) as f: sideline = json.load(f) with open(upstream_path) as f: upstream = json.load(f) # Verify structure assert "frames" in sideline, "Sideline should have frames" assert "frames" in upstream, "Upstream should have frames" assert len(sideline["frames"]) > 0, "Sideline should have at least one frame" assert len(upstream["frames"]) > 0, "Upstream should have at least one frame" def test_sideline_buffer_format(self): """Verify sideline buffer format is plain text.""" sideline_path = Path("output/sideline_demo.json") with open(sideline_path) as f: sideline = json.load(f) # Check first frame frame0 = sideline["frames"][0]["buffer"] # Sideline should have plain text lines (no cursor positioning) # Check first few lines for i, line in enumerate(frame0[:5]): # Should not start with cursor positioning if line.strip(): assert not line.startswith("\033["), ( f"Line {i} should not start with cursor positioning" ) # Should have actual content assert len(line.strip()) > 0, f"Line {i} should have content" def test_upstream_buffer_format(self): """Verify upstream buffer format includes cursor positioning.""" upstream_path = Path("output/upstream_demo.json") with open(upstream_path) as f: upstream = json.load(f) # Check first frame frame0 = upstream["frames"][0]["buffer"] # Upstream should have cursor positioning codes overlay_text = "".join(frame0[:10]) assert "\033[" in overlay_text, "Upstream buffer should contain ANSI codes" assert "H" in overlay_text, "Upstream buffer should contain cursor positioning" if __name__ == "__main__": pytest.main([__file__, "-v"])