Files
Your Name 8447b2d796
mandatory-regression-gate / deterministic-tests (push) Has been cancelled
mandatory-regression-gate / protected-performance-path (push) Has been cancelled
Refactor code structure for improved readability and maintainability
2026-07-23 15:34:05 +03:00

495 lines
14 KiB
Rust

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"));
}