//! Golden Visual Regression Test Harness //! //! Purpose: //! Port v3's deterministic golden hash concept to v4 through the public Engine API boundary. //! These tests validate that Engine output remains deterministic and pixel-perfect across changes. //! //! Design: //! - Uses only `hcie_engine_api::Engine` public API (no internal crate access) //! - Creates deterministic canvases, layers, colors, vector shapes, brush strokes, filters //! - Hashes `engine.get_composite_pixels()` with SHA-256 //! - Compares against inline golden hashes //! //! Regeneration: //! Set `HCIE_REGEN_GOLDENS=1` to print computed hashes after test verification. //! //! Golden cases: //! - white_canvas_empty_document: validates blank document/composite baseline //! - green_rect_draw_and_composite: validates draw_filled_rect_rgba //! - red_rect_over_green_rect: validates layer compositing order //! - vector_rect_golden: validates vector rendering through Engine API //! - brush_stroke_golden: validates brush engine + draw pipeline //! - invert_filter_golden: validates filter pipeline and deterministic output //! - undo_after_rect_golden: validates history snapshot and restoration use hcie_engine_api::Engine; use hcie_engine_api::{BrushStyle, BrushTip, VectorShape}; use sha2::{Digest, Sha256}; /// Compute SHA-256 hash of pixel buffer for golden comparison. fn hash_pixels(pixels: &[u8]) -> String { let mut hasher = Sha256::new(); hasher.update(pixels); format!("{:x}", hasher.finalize()) } /// Test: White canvas, empty document baseline. /// Validates that a blank document produces deterministic all-white/transparent RGBA output. #[test] fn white_canvas_empty_document() { let mut engine = Engine::new(8, 8); let pixels = engine.get_composite_pixels(); // Engine::new creates a transparent document by default assert_eq!(pixels.len(), 8 * 8 * 4, "Canvas size mismatch"); // Transparent pixels: alpha = 0 for i in (0..pixels.len()).step_by(4) { assert_eq!(pixels[i + 3], 0, "Alpha channel should be 0 (transparent)"); } assert_eq!( hash_pixels(&pixels), "5341e6b2646979a70e57653007a1f310169421ec9bdd9f1a5648f75ade005af1" ); } /// Test: Green rectangle draw and composite. /// Validates `draw_filled_rect_rgba` produces deterministic output. #[test] fn green_rect_draw_and_composite() { let mut engine = Engine::new(16, 16); engine.draw_filled_rect_rgba(4, 4, 12, 12, [0, 255, 0, 255]); let pixels = engine.get_composite_pixels(); assert_eq!(pixels.len(), 16 * 16 * 4); // Check center pixel is green let center_idx = (8 * 16 + 8) * 4; assert_eq!(pixels[center_idx], 0); assert_eq!(pixels[center_idx + 1], 255); assert_eq!(pixels[center_idx + 2], 0); assert_eq!(pixels[center_idx + 3], 255); assert_eq!( hash_pixels(&pixels), "01b9681b08e6d9bafd568f110f0656613c7447c667fda4af9350d705dadc758a" ); } /// Test: Red rectangle over green rectangle. /// Validates layer compositing order with two layers. #[test] fn red_rect_over_green_rect() { let mut engine = Engine::new(16, 16); // Bottom layer: green rect let green_id = engine.add_layer("green"); engine.set_active_layer(green_id); engine.draw_filled_rect_rgba(2, 2, 14, 14, [0, 255, 0, 255]); // Top layer: red rect let red_id = engine.add_layer("red"); engine.set_active_layer(red_id); engine.draw_filled_rect_rgba(6, 6, 10, 10, [255, 0, 0, 255]); let pixels = engine.get_composite_pixels(); assert_eq!(pixels.len(), 16 * 16 * 4); // Center pixel should be red (top layer wins) let center_idx = (8 * 16 + 8) * 4; assert_eq!(pixels[center_idx], 255); assert_eq!(pixels[center_idx + 1], 0); assert_eq!(pixels[center_idx + 2], 0); assert_eq!( hash_pixels(&pixels), "c893ae44fb82ff080e286a6625389fef843ac60e82cdb4d7dde8c7975bbae750" ); } /// Test: Vector shape rendering. /// Validates vector shapes are rendered deterministically through Engine API. #[test] fn vector_rect_golden() { let mut engine = Engine::new(16, 16); let shape = VectorShape::Rect { name: String::new(), x1: 0.0, y1: 0.0, x2: 16.0, y2: 16.0, stroke: 1.0, color: [255, 0, 0, 255], fill_color: [255, 0, 0, 255], fill: true, radius: 0.0, angle: 0.0, opacity: 1.0, hardness: 1.0, }; engine.add_vector_shape(shape); let pixels = engine.get_composite_pixels(); assert_eq!(pixels.len(), 16 * 16 * 4); // Verify at least one red pixel (vector rect was drawn) let has_red = pixels .chunks(4) .any(|p| p[0] == 255 && p[1] == 0 && p[2] == 0); assert!(has_red, "Vector rect should contain red pixels"); assert_eq!( hash_pixels(&pixels), "71205eb7a329a3ead670c77eee185c0fbeb612f7a2b3d6aadbe2af4f9276b60d" ); } /// Test: Brush stroke rendering. /// Validates brush engine + draw pipeline produces deterministic output. #[test] fn brush_stroke_golden() { let mut engine = Engine::new(16, 16); let brush = BrushTip { style: BrushStyle::Round, size: 4.0, opacity: 1.0, hardness: 1.0, spacing: 0.1, ..Default::default() }; engine.set_brush_tip(brush); engine.begin_stroke(engine.active_layer_id(), 8.0, 8.0); engine.stroke_to(engine.active_layer_id(), 12.0, 12.0, 1.0); engine.end_stroke(engine.active_layer_id()); let pixels = engine.get_composite_pixels(); assert_eq!(pixels.len(), 16 * 16 * 4); // Brush stroke should produce non-white pixels let has_non_white = pixels.iter().any(|&p| p < 255); assert!( has_non_white, "Brush stroke should produce non-white pixels" ); assert_eq!( hash_pixels(&pixels), "e97f0dd89644a40cd4d16b7440576265761849c45180fd4dece66b4f709fa087" ); } /// Test: Invert filter. /// Validates filter pipeline produces deterministic output. #[test] fn invert_filter_golden() { let mut engine = Engine::new(8, 8); engine.draw_filled_rect_rgba(0, 0, 8, 8, [100, 150, 200, 255]); engine.apply_filter("invert", serde_json::json!({})); let pixels = engine.get_composite_pixels(); assert_eq!(pixels.len(), 8 * 8 * 4); // Center pixel should be inverted let idx = (4 * 8 + 4) * 4; assert_eq!(pixels[idx], 155); // 255 - 100 assert_eq!(pixels[idx + 1], 105); // 255 - 150 assert_eq!(pixels[idx + 2], 55); // 255 - 200 assert_eq!( hash_pixels(&pixels), "35490247d911e119aeae86afa584459b47b853262afde75f832521a738bb069f" ); } /// Test: Undo after rectangle. /// Validates history snapshot and restoration works correctly. #[test] fn undo_after_rect_golden() { let mut engine = Engine::new(8, 8); // Initial state: transparent let before = hash_pixels(&engine.get_composite_pixels()); // Draw green rect engine.draw_filled_rect_rgba(2, 2, 6, 6, [0, 255, 0, 255]); // Undo engine.undo(); let after = hash_pixels(&engine.get_composite_pixels()); // After undo, should match initial state assert_eq!(before, after, "Undo should restore original canvas state"); } /// Test: Vector fill toggle and delete. /// Validates that set_vector_shape_fill and delete_vector_shape work correctly. #[test] fn vector_fill_toggle_and_delete() { let mut engine = Engine::new(16, 16); // Create a rect with fill=false let shape = VectorShape::Rect { name: String::new(), x1: 2.0, y1: 2.0, x2: 14.0, y2: 14.0, stroke: 1.0, color: [255, 0, 0, 255], fill_color: [0, 0, 255, 255], fill: false, radius: 0.0, angle: 0.0, opacity: 1.0, hardness: 1.0, }; engine.add_vector_shape(shape); let layer_id = engine.active_layer_id(); // Verify fill is initially false let shapes = engine.active_vector_shapes().unwrap(); assert_eq!( shapes[0].fill(), Some(false), "fill should be false initially" ); // Toggle fill ON engine.set_vector_shape_fill(layer_id, 0, true); let shapes = engine.active_vector_shapes().unwrap(); assert_eq!( shapes[0].fill(), Some(true), "fill should be true after toggle" ); // Verify composite pixels contain blue fill let pixels = engine.get_composite_pixels(); let has_blue = pixels .chunks(4) .any(|p| p[2] == 255 && p[0] == 0 && p[1] == 0 && p[3] > 0); assert!(has_blue, "should have blue fill pixels after enabling fill"); // Toggle fill OFF engine.set_vector_shape_fill(layer_id, 0, false); let shapes = engine.active_vector_shapes().unwrap(); assert_eq!( shapes[0].fill(), Some(false), "fill should be false after toggling off" ); // Delete the shape engine.delete_vector_shape(layer_id, 0); let shapes = engine.active_vector_shapes().unwrap(); assert!(shapes.is_empty(), "shapes should be empty after delete"); }