"""Visual regression tests for flashcard generation.""" import pytest import subprocess import tempfile import imagehash from pathlib import Path from PIL import Image import sys # Import our modules from generate import generate_cards_direct class TestVisualRegression: """Visual regression tests using image hashing and comparison.""" def test_basic_card_generation(self, temp_dir, sample_config, reference_images_dir): """Test basic card generation produces consistent output.""" # Generate a single test card (number 7) numbers = [7] config = { **sample_config, 'transparent': False, 'card_width': '2in', # Smaller for faster tests 'card_height': '1.4in' } # Generate test output test_output = temp_dir / 'test_cards' generated_files = generate_cards_direct( numbers, config, test_output, format='png', dpi=150, # Lower DPI for faster tests separate_fronts_backs=True ) assert len(generated_files) == 2 # Front and back # Check that files were created front_file = test_output / 'fronts' / 'card_000.png' back_file = test_output / 'backs' / 'card_000.png' assert front_file.exists(), "Front card image should exist" assert back_file.exists(), "Back card image should exist" # Basic image validation front_img = Image.open(front_file) back_img = Image.open(back_file) # Check dimensions are reasonable assert front_img.size[0] > 100, "Front image should have reasonable width" assert front_img.size[1] > 100, "Front image should have reasonable height" assert back_img.size[0] > 100, "Back image should have reasonable width" assert back_img.size[1] > 100, "Back image should have reasonable height" # Store as reference if they don't exist ref_front = reference_images_dir / 'card_7_front.png' ref_back = reference_images_dir / 'card_7_back.png' if not ref_front.exists(): front_img.save(ref_front) print(f"Created reference image: {ref_front}") if not ref_back.exists(): back_img.save(ref_back) print(f"Created reference image: {ref_back}") # If references exist, compare hashes if ref_front.exists() and ref_back.exists(): ref_front_img = Image.open(ref_front) ref_back_img = Image.open(ref_back) # Use perceptual hashing for reasonable tolerance front_hash = imagehash.phash(front_img) ref_front_hash = imagehash.phash(ref_front_img) back_hash = imagehash.phash(back_img) ref_back_hash = imagehash.phash(ref_back_img) # Allow small differences (hash distance < 5) assert front_hash - ref_front_hash < 5, f"Front card changed significantly (hash diff: {front_hash - ref_front_hash})" assert back_hash - ref_back_hash < 5, f"Back card changed significantly (hash diff: {back_hash - ref_back_hash})" def test_different_bead_shapes(self, temp_dir, sample_config, reference_images_dir): """Test that different bead shapes produce visually different outputs.""" numbers = [5] # Simple number for shape testing base_config = { **sample_config, 'card_width': '2in', 'card_height': '1.4in' } shapes = ['diamond', 'circle', 'square'] shape_hashes = {} for shape in shapes: config = {**base_config, 'bead_shape': shape} shape_output = temp_dir / f'shape_{shape}' generated_files = generate_cards_direct( numbers, config, shape_output, format='png', dpi=150, separate_fronts_backs=True ) assert len(generated_files) == 2 # Get hash of front image (where shape is visible) front_file = shape_output / 'fronts' / 'card_000.png' front_img = Image.open(front_file) shape_hashes[shape] = imagehash.phash(front_img) # Verify shapes produce different images assert shape_hashes['diamond'] - shape_hashes['circle'] > 3, "Diamond and circle shapes should be visually different" assert shape_hashes['diamond'] - shape_hashes['square'] > 3, "Diamond and square shapes should be visually different" assert shape_hashes['circle'] - shape_hashes['square'] > 3, "Circle and square shapes should be visually different" def test_color_schemes(self, temp_dir, sample_config, reference_images_dir): """Test that different color schemes produce different outputs.""" numbers = [23] # Multi-digit number for color testing base_config = { **sample_config, 'card_width': '2in', 'card_height': '1.4in' } schemes = ['monochrome', 'place-value'] scheme_hashes = {} for scheme in schemes: config = {**base_config, 'color_scheme': scheme} scheme_output = temp_dir / f'color_{scheme}' generated_files = generate_cards_direct( numbers, config, scheme_output, format='png', dpi=150, separate_fronts_backs=True ) assert len(generated_files) == 2 # Get hash of front image front_file = scheme_output / 'fronts' / 'card_000.png' front_img = Image.open(front_file) scheme_hashes[scheme] = imagehash.phash(front_img) # Color schemes should produce different images hash_diff = scheme_hashes['monochrome'] - scheme_hashes['place-value'] assert hash_diff >= 2, f"Color schemes should be visually different (hash diff: {hash_diff})" @pytest.mark.slow def test_pdf_generation_structure(self, temp_dir, sample_config): """Test that PDF generation produces valid PDF files.""" # This test requires typst to be installed try: subprocess.run(['typst', '--version'], capture_output=True, check=True) except (subprocess.CalledProcessError, FileNotFoundError): pytest.skip("Typst not installed - skipping PDF tests") from generate import generate_typst_file numbers = [1, 2, 3] output_typst = temp_dir / 'test.typ' output_pdf = temp_dir / 'test.pdf' # Generate Typst file generate_typst_file(numbers, sample_config, output_typst) # Check Typst file was created and has content assert output_typst.exists() assert output_typst.stat().st_size > 100 # Should have substantial content # Try to compile to PDF (if typst is available) try: result = subprocess.run([ 'typst', 'compile', '--font-path', str(Path(__file__).parent.parent / 'fonts'), str(output_typst), str(output_pdf) ], capture_output=True, text=True, cwd=temp_dir) if result.returncode == 0: assert output_pdf.exists(), "PDF should be generated" assert output_pdf.stat().st_size > 1000, "PDF should have reasonable size" else: print(f"PDF compilation failed: {result.stderr}") # Don't fail the test - typst might have font issues in test environment except FileNotFoundError: pytest.skip("Typst not available for PDF compilation")