forked from genewildish/Mainline
test(verification): Add visual verification tests for message overlay
This commit is contained in:
234
tests/test_visual_verification.py
Normal file
234
tests/test_visual_verification.py
Normal file
@@ -0,0 +1,234 @@
|
||||
"""
|
||||
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"])
|
||||
Reference in New Issue
Block a user