use std::path::PathBuf; /// Integration tests for hcie-document crate. /// /// Covers: document creation, layer CRUD, active layer switching, /// visibility/opacity/blend-mode management, dirty tracking, zoom/pan, /// undo/redo, selection, crop/resize. // --------------------------------------------------------------------------- // Helper: create a small test document // --------------------------------------------------------------------------- fn make_doc() -> hcie_document::Document { hcie_document::Document::new_blank("test", 64, 64, false) } fn make_transparent_doc() -> hcie_document::Document { hcie_document::Document::new_blank("transparent", 64, 64, true) } // --------------------------------------------------------------------------- // Document creation // --------------------------------------------------------------------------- #[test] fn new_blank_document_has_correct_dimensions() { let doc = make_doc(); assert_eq!(doc.canvas_width, 64); assert_eq!(doc.canvas_height, 64); } #[test] fn new_blank_document_has_one_layer() { let doc = make_doc(); assert_eq!(doc.layers.len(), 1, "new blank document must have exactly one layer"); } #[test] fn new_blank_document_name_is_set() { let doc = make_doc(); assert_eq!(doc.name, "test"); } #[test] fn new_blank_opaque_layer_is_white() { let doc = make_doc(); let layer = doc.active_layer().unwrap(); let center = layer.get_pixel(32, 32); assert_eq!(center, [255, 255, 255, 255], "opaque background should be white"); } #[test] fn new_transparent_layer_is_empty() { let doc = make_transparent_doc(); let layer = doc.active_layer().unwrap(); let center = layer.get_pixel(32, 32); assert_eq!(center, [0, 0, 0, 0], "transparent background should be all zeros"); } #[test] fn initial_zoom_is_one() { let doc = make_doc(); assert!((doc.zoom - 1.0).abs() < f32::EPSILON); } #[test] fn initial_pan_is_zero() { let doc = make_doc(); assert!((doc.pan_x).abs() < f32::EPSILON); assert!((doc.pan_y).abs() < f32::EPSILON); } // --------------------------------------------------------------------------- // Tab label & modified flag // --------------------------------------------------------------------------- #[test] fn tab_label_shows_modified_indicator() { let mut doc = make_doc(); assert!(!doc.tab_label().contains('●'), "clean doc should not show modified indicator"); doc.modified = true; assert!(doc.tab_label().contains('●'), "modified doc should show the dot"); } // --------------------------------------------------------------------------- // Layer CRUD // --------------------------------------------------------------------------- #[test] fn add_layer_increases_count() { let mut doc = make_doc(); let id = doc.add_layer("Layer 2"); assert_eq!(doc.layers.len(), 2); assert!(id > 0); } #[test] fn add_layer_makes_it_active() { let mut doc = make_doc(); let id = doc.add_layer("Layer 2"); // add_layer pushes to the end → index 1 when background is at 0 assert_eq!(doc.active_layer, 1, "new layer should be active (last index)"); } #[test] fn delete_layer_reduces_count() { let mut doc = make_doc(); doc.add_layer("Layer 2"); doc.delete_layer(1); assert_eq!(doc.layers.len(), 1); } #[test] fn delete_last_layer_removes_it() { let mut doc = make_doc(); assert_eq!(doc.layers.len(), 1); doc.delete_layer(0); // The layer is removed; active_layer is clamped to the new (empty) len assert_eq!(doc.layers.len(), 0, "deleting the only layer should remove it"); assert_eq!(doc.active_layer, 0, "active_layer should clamp to 0"); } #[test] fn delete_layer_out_of_bounds_is_noop() { let mut doc = make_doc(); doc.delete_layer(99); assert_eq!(doc.layers.len(), 1); } #[test] fn move_layer_changes_order() { let mut doc = make_doc(); doc.add_layer("Layer A"); // index 1 doc.add_layer("Layer B"); // index 2 // Move "Layer B" down to index 1 (before "Layer A") doc.move_layer(2, 1); assert_eq!(doc.layers[0].name, "Background"); assert_eq!(doc.layers[1].name, "Layer B"); assert_eq!(doc.layers[2].name, "Layer A"); } #[test] fn move_layer_identity_is_noop() { let mut doc = make_doc(); doc.add_layer("L2"); doc.move_layer(1, 1); assert_eq!(doc.layers.len(), 2); } #[test] fn all_layer_ids_returns_all() { let mut doc = make_doc(); doc.add_layer("L2"); doc.add_layer("L3"); let ids = doc.all_layer_ids(); assert_eq!(ids.len(), 3); } #[test] fn layer_info_returns_correct_data() { let mut doc = make_doc(); let id = doc.add_layer("MyLayer"); let info = doc.layer_info(id).unwrap(); assert_eq!(info.name, "MyLayer"); assert!(info.visible); assert!((info.opacity - 1.0).abs() < 1e-6); } #[test] fn layer_info_returns_none_for_invalid_id() { let doc = make_doc(); assert!(doc.layer_info(99999).is_none()); } #[test] fn get_layer_by_id_finds_layer() { let mut doc = make_doc(); let id = doc.add_layer("Target"); let layer = doc.get_layer_by_id(id).unwrap(); assert_eq!(layer.name, "Target"); } #[test] fn get_layer_by_id_returns_none_for_missing() { let doc = make_doc(); assert!(doc.get_layer_by_id(99999).is_none()); } #[test] fn layer_index_by_id_returns_correct_index() { let mut doc = make_doc(); let id = doc.add_layer("L2"); // add_layer pushes to the end; background is index 0, L2 is index 1 let idx = doc.layer_index_by_id(id).unwrap(); assert_eq!(idx, 1); } // --------------------------------------------------------------------------- // Active layer // --------------------------------------------------------------------------- #[test] fn set_active_layer_switches_to_valid_index() { let mut doc = make_doc(); doc.add_layer("L2"); doc.set_active_layer(1); assert_eq!(doc.active_layer, 1); } #[test] fn active_layer_returns_correct_layer() { let mut doc = make_doc(); let layer = doc.active_layer().unwrap(); assert_eq!(layer.name, "Background"); } #[test] fn active_layer_mut_allows_modification() { let mut doc = make_doc(); { let layer = doc.active_layer_mut().unwrap(); layer.name = "Renamed".to_string(); } assert_eq!(doc.layers[doc.active_layer].name, "Renamed"); } // --------------------------------------------------------------------------- // Visibility // --------------------------------------------------------------------------- #[test] fn set_layer_visible_hides_layer() { let mut doc = make_doc(); doc.set_layer_visible(0, false); assert!(!doc.layers[0].visible); } #[test] fn set_layer_visible_shows_layer() { let mut doc = make_doc(); doc.set_layer_visible(0, false); doc.set_layer_visible(0, true); assert!(doc.layers[0].visible); } #[test] fn layer_visible_default_is_true() { let doc = make_doc(); assert!(doc.layers[0].visible); } // --------------------------------------------------------------------------- // Blend mode & opacity (via direct field access through mutable accessor) // --------------------------------------------------------------------------- #[test] fn set_blend_mode_via_mut_accessor() { let mut doc = make_doc(); let layer = doc.active_layer_mut().unwrap(); layer.blend_mode = hcie_protocol::BlendMode::Multiply; assert_eq!(doc.layers[doc.active_layer].blend_mode, hcie_protocol::BlendMode::Multiply); } #[test] fn set_opacity_via_mut_accessor() { let mut doc = make_doc(); let layer = doc.active_layer_mut().unwrap(); layer.opacity = 0.5; assert!((doc.layers[doc.active_layer].opacity - 0.5).abs() < 1e-6); } #[test] fn opacity_clamps_to_zero() { let mut doc = make_doc(); let layer = doc.active_layer_mut().unwrap(); layer.opacity = -0.1; // Protocol allows negative; consumer should clamp assert!(layer.opacity < 0.0); } // --------------------------------------------------------------------------- // Dirty tracking // --------------------------------------------------------------------------- #[test] fn expand_dirty_produces_valid_bounds() { let mut doc = make_doc(); doc.expand_dirty(10, 10, 5.0); let bounds = doc.dirty_bounds; assert!(bounds.is_some(), "dirty bounds should be set after expand_dirty"); if let Some([x0, y0, x1, y1]) = bounds { assert!(x0 <= x1); assert!(y0 <= y1); } } #[test] fn clear_dirty_resets_bounds() { let mut doc = make_doc(); doc.expand_dirty(10, 10, 5.0); doc.clear_dirty(); assert!(doc.dirty_bounds.is_none(), "dirty bounds should be None after clear"); assert!(!doc.composite_dirty, "composite_dirty should be false after clear"); } #[test] fn expand_dirty_merges_with_existing() { let mut doc = make_doc(); doc.expand_dirty(0, 0, 1.0); doc.expand_dirty(50, 50, 1.0); let bounds = doc.dirty_bounds.unwrap(); // Should cover both regions assert!(bounds[0] <= 1, "x0 should be near 0"); assert!(bounds[2] >= 49, "x1 should cover second region"); } // --------------------------------------------------------------------------- // Zoom and pan // --------------------------------------------------------------------------- #[test] fn zoom_can_be_set() { let mut doc = make_doc(); doc.zoom = 2.0; assert!((doc.zoom - 2.0).abs() < f32::EPSILON); } #[test] fn zoom_can_be_negative() { let mut doc = make_doc(); doc.zoom = -1.0; // Negative zoom should be allowed (clamped at display layer) assert!(doc.zoom < 0.0); } #[test] fn pan_can_be_set() { let mut doc = make_doc(); doc.pan_x = 100.0; doc.pan_y = -50.0; assert!((doc.pan_x - 100.0).abs() < f32::EPSILON); assert!((doc.pan_y - (-50.0)).abs() < f32::EPSILON); } // --------------------------------------------------------------------------- // Selection // --------------------------------------------------------------------------- #[test] fn set_selection_rect_activates_selection() { let mut doc = make_doc(); doc.set_selection_rect((10, 10), (30, 30)); assert!(doc.selection_active); } #[test] fn clear_selection_deactivates() { let mut doc = make_doc(); doc.set_selection_rect((10, 10), (30, 30)); doc.clear_selection(); assert!(!doc.selection_active); } #[test] fn select_all_covers_full_canvas() { let mut doc = make_doc(); doc.select_all(); assert!(doc.selection_active); if let Some(ref mask) = doc.selection_mask { assert_eq!(mask.len(), (64 * 64) as usize); assert!(mask.iter().all(|&v| v == 255), "select_all should set all mask bytes to 255"); } } #[test] fn get_mask_at_returns_255_when_no_selection() { let doc = make_doc(); assert_eq!(doc.get_mask_at(0, 0), 255, "without selection mask, get_mask_at should return 255"); } #[test] fn get_mask_at_returns_255_inside_selection() { let mut doc = make_doc(); doc.select_all(); assert_eq!(doc.get_mask_at(32, 32), 255, "inside selection should return 255"); } // --------------------------------------------------------------------------- // Crop // --------------------------------------------------------------------------- #[test] fn crop_reduces_canvas_size() { let mut doc = make_doc(); doc.crop(0, 0, 32, 32); assert_eq!(doc.canvas_width, 32); assert_eq!(doc.canvas_height, 32); } // --------------------------------------------------------------------------- // Resize // --------------------------------------------------------------------------- #[test] fn resize_canvas_increases_size() { let mut doc = make_doc(); doc.resize_canvas(128, 128); assert_eq!(doc.canvas_width, 128); assert_eq!(doc.canvas_height, 128); } #[test] fn resize_canvas_does_not_shrink_below_one() { let mut doc = make_doc(); doc.resize_canvas(0, 0); // Minimum size should be at least 1 assert!(doc.canvas_width >= 1); assert!(doc.canvas_height >= 1); } // --------------------------------------------------------------------------- // History / Undo-Redo // --------------------------------------------------------------------------- #[test] fn new_document_has_no_undo() { let doc = make_doc(); assert!(!doc.can_undo()); assert!(!doc.can_redo()); } #[test] fn add_layer_creates_undoable_action() { let mut doc = make_doc(); doc.add_layer("Undoable"); assert!(doc.can_undo(), "add_layer should record a history entry"); } #[test] fn undo_redo_layer_addition() { let mut doc = make_doc(); let count_before = doc.layers.len(); doc.add_layer("Temp"); assert_eq!(doc.layers.len(), count_before + 1); doc.undo(); // After undo, layers should be restored to previous state assert!(doc.can_redo(), "should be able to redo after undo"); } #[test] fn history_len_increases_with_actions() { let mut doc = make_doc(); assert_eq!(doc.history_len(), 0, "fresh document should have 0 history entries (initialization not recorded)"); doc.add_layer("Action 1"); doc.add_layer("Action 2"); assert!(doc.history_len() >= 2); } #[test] fn history_description_is_readable() { let mut doc = make_doc(); doc.add_layer("My Layer"); if doc.history_len() > 0 { let desc = doc.history_description(doc.history_len() - 1); assert!(desc.is_some()); assert!(!desc.unwrap().is_empty()); } } #[test] fn history_current_index_is_valid() { let mut doc = make_doc(); assert!(doc.history_current() >= -1); } // --------------------------------------------------------------------------- // Layer pixel manipulation // --------------------------------------------------------------------------- #[test] fn get_pixel_returns_correct_value() { let doc = make_doc(); let pixel = doc.layers[0].get_pixel(0, 0); assert_eq!(pixel.len(), 4); } // --------------------------------------------------------------------------- // Layer data & type // --------------------------------------------------------------------------- #[test] fn layer_default_type_is_raster() { let mut doc = make_doc(); let id = doc.add_layer("Raster"); let info = doc.layer_info(id).unwrap(); assert!(matches!(info.layer_type, hcie_protocol::LayerType::Raster)); } #[test] fn file_path_defaults_to_none() { let doc = make_doc(); assert!(doc.file_path.is_none()); } #[test] fn file_path_can_be_set() { let mut doc = make_doc(); doc.file_path = Some(PathBuf::from("/tmp/test.hcie")); assert_eq!(doc.file_path.as_ref().unwrap(), &PathBuf::from("/tmp/test.hcie")); }