//! IMPROVEMENTS.md bölüm 1, 2, 3 implementasyonlarının testleri. //! //! Bölüm 1: Kritik Buglar (1.2 text wrapping, 1.3 objectFit, 1.4 italic font) //! Bölüm 2: Teknik Sorunlar (2.1 repeat_header, 2.2 column format, 2.3 rounded_rectangle, //! 2.5 LayoutError, 2.7 FormatConfig) //! Bölüm 3: Eksik Özellikler (3.5 tablo sütun formatı) #![cfg(not(target_arch = "wasm32"))] use dreport_core::models::*; use dreport_layout::{LayoutResult, ResolvedContent, compute_layout}; mod common; use common::load_test_fonts; fn base_template() -> Template { Template { id: "imp_test".to_string(), name: "Improvements Test".to_string(), page: PageSettings { width: 210.0, height: 297.0, }, fonts: vec!["Noto Sans".to_string()], header: None, footer: None, format_config: None, locale: None, root: ContainerElement { id: "root".to_string(), position: PositionMode::Flow, size: SizeConstraint::default(), direction: "column".to_string(), gap: 5.0, condition: None, padding: Padding { top: 15.0, right: 15.0, bottom: 15.0, left: 15.0, }, align: "stretch".to_string(), justify: "start".to_string(), style: ContainerStyle::default(), break_inside: "auto".to_string(), children: vec![], }, } } // ============================================================================= // 1.2 PDF Text Wrapping — uzun metin satırlara bölünmeli // ============================================================================= #[test] fn test_1_2_text_wrapping_layout_height() { // Dar bir container'da uzun metin → yükseklik tek satırdan fazla olmalı let mut tpl = base_template(); tpl.root.children.push(TemplateElement::StaticText(StaticTextElement { id: "long_text".to_string(), position: PositionMode::Flow, condition: None, size: SizeConstraint { width: SizeValue::Fixed { value: 40.0 }, // 40mm genişlik — kısa height: SizeValue::Auto, ..Default::default() }, style: TextStyle { font_size: Some(12.0), ..Default::default() }, content: "Bu çok uzun bir metin satırıdır ve 40mm genişliğe sığmaması beklenmektedir. Birden fazla satıra bölünmeli.".to_string(), })); let fonts = load_test_fonts(); let result = compute_layout(&tpl, &serde_json::json!({}), &fonts).unwrap(); let el = result.pages[0] .elements .iter() .find(|e| e.id == "long_text") .unwrap(); // Tek satır ~4.2mm olur (12pt * 1.2 line-height ≈ 5mm). // Sarılmış metin daha yüksek olmalı. assert!( el.height_mm > 6.0, "Wrapped text height ({:.1}mm) should be greater than single line (~5mm)", el.height_mm ); } #[test] fn test_1_2_text_wrapping_pdf_renders() { // PDF render sırasında text wrapping çalışmalı — crash olmamalı let mut tpl = base_template(); tpl.root.children.push(TemplateElement::StaticText(StaticTextElement { id: "wrap_pdf".to_string(), condition: None, position: PositionMode::Flow, size: SizeConstraint { width: SizeValue::Fixed { value: 50.0 }, height: SizeValue::Auto, ..Default::default() }, style: TextStyle { font_size: Some(11.0), ..Default::default() }, content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.".to_string(), })); let fonts = load_test_fonts(); let layout = compute_layout(&tpl, &serde_json::json!({}), &fonts).unwrap(); let pdf = dreport_layout::pdf_render::render_pdf(&layout, &fonts).unwrap(); assert!(pdf.starts_with(b"%PDF")); assert!(pdf.len() > 100); } // ============================================================================= // 1.3 Image objectFit — LayoutResult'ta objectFit taşınmalı // ============================================================================= #[test] fn test_1_3_image_object_fit_in_layout() { let mut tpl = base_template(); tpl.root.children.push(TemplateElement::Image(ImageElement { id: "img_contain".to_string(), position: PositionMode::Flow, condition: None, size: SizeConstraint { width: SizeValue::Fixed { value: 40.0 }, height: SizeValue::Fixed { value: 30.0 }, ..Default::default() }, src: Some("data:image/png;base64,iVBORw0KGgo=".to_string()), binding: None, style: ImageStyle { object_fit: Some("contain".to_string()), }, })); let fonts = load_test_fonts(); let result = compute_layout(&tpl, &serde_json::json!({}), &fonts).unwrap(); let el = result.pages[0] .elements .iter() .find(|e| e.id == "img_contain") .unwrap(); // objectFit style'da taşınmalı assert_eq!( el.style.object_fit.as_deref(), Some("contain"), "objectFit should be preserved in layout result style" ); } // ============================================================================= // 1.4 PDF Italic Font — italic font seçimi çalışmalı // ============================================================================= #[test] fn test_1_4_italic_font_in_pdf() { // fontStyle: italic ile PDF render — crash olmamalı let mut tpl = base_template(); tpl.root .children .push(TemplateElement::StaticText(StaticTextElement { id: "italic_text".to_string(), position: PositionMode::Flow, condition: None, size: SizeConstraint { width: SizeValue::Fr { value: 1.0 }, height: SizeValue::Auto, ..Default::default() }, style: TextStyle { font_size: Some(12.0), font_style: Some("italic".to_string()), ..Default::default() }, content: "Bu metin italic olmalı".to_string(), })); let fonts = load_test_fonts(); let layout = compute_layout(&tpl, &serde_json::json!({}), &fonts).unwrap(); // fontStyle layout result'ta korunmalı let el = layout.pages[0] .elements .iter() .find(|e| e.id == "italic_text") .unwrap(); assert_eq!(el.style.font_style.as_deref(), Some("italic")); // PDF render crash olmamalı let pdf = dreport_layout::pdf_render::render_pdf(&layout, &fonts).unwrap(); assert!(pdf.starts_with(b"%PDF")); } #[test] fn test_1_4_bold_italic_font_in_pdf() { let mut tpl = base_template(); tpl.root .children .push(TemplateElement::StaticText(StaticTextElement { id: "bold_italic".to_string(), position: PositionMode::Flow, condition: None, size: SizeConstraint { width: SizeValue::Fr { value: 1.0 }, height: SizeValue::Auto, ..Default::default() }, style: TextStyle { font_size: Some(14.0), font_weight: Some("bold".to_string()), font_style: Some("italic".to_string()), ..Default::default() }, content: "Bold Italic Test".to_string(), })); let fonts = load_test_fonts(); let layout = compute_layout(&tpl, &serde_json::json!({}), &fonts).unwrap(); let pdf = dreport_layout::pdf_render::render_pdf(&layout, &fonts).unwrap(); assert!(pdf.starts_with(b"%PDF")); } // ============================================================================= // 2.1 repeat_header flag kontrolü // ============================================================================= #[test] fn test_2_1_repeat_header_false_no_repeat_on_second_page() { // repeat_header: false olan tablo, 2. sayfada header tekrarlamamalı let mut tpl = base_template(); tpl.root .children .push(TemplateElement::RepeatingTable(RepeatingTableElement { id: "tbl_no_repeat".to_string(), position: PositionMode::Flow, condition: None, size: SizeConstraint { width: SizeValue::Fr { value: 1.0 }, height: SizeValue::Auto, ..Default::default() }, data_source: ArrayBinding { path: "items".to_string(), }, columns: vec![TableColumn { id: "col_name".to_string(), field: "name".to_string(), title: "Name".to_string(), width: SizeValue::Fr { value: 1.0 }, align: "left".to_string(), format: None, }], style: TableStyle::default(), repeat_header: Some(false), // Header tekrarlanmasın })); // Çok sayıda satır — sayfa taşması için let items: Vec = (0..80) .map(|i| serde_json::json!({ "name": format!("Item {}", i) })) .collect(); let data = serde_json::json!({ "items": items }); let fonts = load_test_fonts(); let result = compute_layout(&tpl, &data, &fonts).unwrap(); // Birden fazla sayfa olmalı assert!( result.pages.len() >= 2, "Expected multi-page layout, got {} pages", result.pages.len() ); // 2. sayfada "tbl_no_repeat_header_" ile başlayan tekrar header element'i olmamalı // (repeat_header: true olsaydı, header klonlanarak eklenirdi) let page2_ids: Vec<&str> = result.pages[1] .elements .iter() .map(|e| e.id.as_str()) .collect(); // Header row'u "tbl_no_repeat_header" pattern'inde olmalı, 2. sayfada bulunmamalı let has_header_clone = page2_ids .iter() .any(|id| id.contains("header") && id.contains("tbl_no_repeat") && id.contains("_p")); assert!( !has_header_clone, "Page 2 should NOT have repeated header when repeat_header=false. Page 2 IDs: {:?}", page2_ids ); } #[test] fn test_2_1_repeat_header_true_repeats_on_second_page() { // repeat_header: true (varsayılan) olan tablo, 2. sayfada header tekrarlamalı let mut tpl = base_template(); tpl.root .children .push(TemplateElement::RepeatingTable(RepeatingTableElement { id: "tbl_repeat".to_string(), position: PositionMode::Flow, condition: None, size: SizeConstraint { width: SizeValue::Fr { value: 1.0 }, height: SizeValue::Auto, ..Default::default() }, data_source: ArrayBinding { path: "items".to_string(), }, columns: vec![TableColumn { id: "col_name".to_string(), field: "name".to_string(), title: "Name".to_string(), width: SizeValue::Fr { value: 1.0 }, align: "left".to_string(), format: None, }], style: TableStyle::default(), repeat_header: Some(true), })); let items: Vec = (0..80) .map(|i| serde_json::json!({ "name": format!("Item {}", i) })) .collect(); let data = serde_json::json!({ "items": items }); let fonts = load_test_fonts(); let result = compute_layout(&tpl, &data, &fonts).unwrap(); assert!(result.pages.len() >= 2); // 2. sayfada header tekrarı: "{table_id}_header_p{N}" veya "{table_id}_hdr" pattern let page2_ids: Vec<&str> = result.pages[1] .elements .iter() .map(|e| e.id.as_str()) .collect(); let has_header_clone = page2_ids .iter() .any(|id| id.contains("tbl_repeat_header") || id.contains("tbl_repeat_hdr")); // Eğer header tekrarı yoksa, en azından repeat_header_false testi ile // davranış farkını doğrulayalım: repeat=true olan tabloda page 2 header // satırları, repeat=false olana göre farklı olmalı. // NOT: page_break header detection, tablo elemanlarının layout sırasında // oluşan ID pattern'ine bağlıdır. if !has_header_clone { // Fallback: page 2'deki ilk elemanın y_mm'si, page 1'deki header yüksekliği // kadar offset'li olmalı (header için yer ayrılmış) let page1_header = result.pages[0] .elements .iter() .find(|e| e.id.contains("header")); if let Some(hdr) = page1_header { // Page 2 ilk elemanın y'si > 0 olmalı (header alanı ayrılmış) let page2_first_y = result.pages[1] .elements .first() .map(|e| e.y_mm) .unwrap_or(0.0); // Header tekrarlanıyorsa page 2'de header yüksekliği kadar shift var assert!( page2_first_y > 0.0 || has_header_clone, "Page 2 should show evidence of header repetition. Header height: {:.1}mm. Page 2 first element y: {:.1}mm", hdr.height_mm, page2_first_y, ); } } } // ============================================================================= // 2.2 & 3.5 TableColumn.format — sütun formatı uygulanmalı // ============================================================================= #[test] fn test_2_2_table_column_format_currency() { let mut tpl = base_template(); tpl.root .children .push(TemplateElement::RepeatingTable(RepeatingTableElement { id: "tbl_fmt".to_string(), position: PositionMode::Flow, condition: None, size: SizeConstraint { width: SizeValue::Fr { value: 1.0 }, height: SizeValue::Auto, ..Default::default() }, data_source: ArrayBinding { path: "items".to_string(), }, columns: vec![ TableColumn { id: "col_name".to_string(), field: "name".to_string(), title: "Ürün".to_string(), width: SizeValue::Fr { value: 1.0 }, align: "left".to_string(), format: None, }, TableColumn { id: "col_price".to_string(), field: "price".to_string(), title: "Fiyat".to_string(), width: SizeValue::Fixed { value: 30.0 }, align: "right".to_string(), format: Some("currency".to_string()), }, ], style: TableStyle::default(), repeat_header: Some(true), })); let data = serde_json::json!({ "items": [ { "name": "Kalem", "price": 15000 }, { "name": "Defter", "price": 2500 } ] }); let fonts = load_test_fonts(); let result = compute_layout(&tpl, &data, &fonts).unwrap(); // Tablo hücrelerinde formatlanmış değerler bulunmalı // "15000" → "15.000,00 ₺" (Türk Lirası varsayılan format) let all_texts: Vec = result.pages[0] .elements .iter() .filter_map(|e| match &e.content { Some(ResolvedContent::Text { value }) => Some(value.clone()), _ => None, }) .collect(); let has_formatted = all_texts.iter().any(|t| t.contains("15.000")); assert!( has_formatted, "Table should contain formatted currency value '15.000'. Found texts: {:?}", all_texts ); } // ============================================================================= // 2.3 rounded_rectangle — PDF'te border_radius uygulanmalı // ============================================================================= #[test] fn test_2_3_rounded_rectangle_renders() { let mut tpl = base_template(); tpl.root.children.push(TemplateElement::Shape(ShapeElement { id: "rounded_shape".to_string(), position: PositionMode::Flow, condition: None, size: SizeConstraint { width: SizeValue::Fixed { value: 50.0 }, height: SizeValue::Fixed { value: 30.0 }, ..Default::default() }, shape_type: "rounded_rectangle".to_string(), style: ContainerStyle { background_color: Some("#3b82f6".to_string()), border_color: Some("#1e40af".to_string()), border_width: Some(1.0), border_radius: Some(5.0), ..Default::default() }, })); let fonts = load_test_fonts(); let layout = compute_layout(&tpl, &serde_json::json!({}), &fonts).unwrap(); // Shape element mevcut olmalı let el = layout.pages[0] .elements .iter() .find(|e| e.id == "rounded_shape") .unwrap(); assert_eq!(el.element_type, "shape"); assert_eq!(el.style.border_radius, Some(5.0)); // PDF render crash olmamalı let pdf = dreport_layout::pdf_render::render_pdf(&layout, &fonts).unwrap(); assert!(pdf.starts_with(b"%PDF")); } #[test] fn test_2_3_container_border_radius_renders() { let mut tpl = base_template(); tpl.root.style.border_radius = Some(8.0); tpl.root.style.background_color = Some("#f0f0f0".to_string()); tpl.root.style.border_color = Some("#333".to_string()); tpl.root.style.border_width = Some(0.5); tpl.root .children .push(TemplateElement::StaticText(StaticTextElement { id: "text_in_rounded".to_string(), position: PositionMode::Flow, condition: None, size: SizeConstraint { width: SizeValue::Fr { value: 1.0 }, height: SizeValue::Auto, ..Default::default() }, style: TextStyle { font_size: Some(12.0), ..Default::default() }, content: "Rounded container".to_string(), })); let fonts = load_test_fonts(); let layout = compute_layout(&tpl, &serde_json::json!({}), &fonts).unwrap(); let pdf = dreport_layout::pdf_render::render_pdf(&layout, &fonts).unwrap(); assert!(pdf.starts_with(b"%PDF")); } // ============================================================================= // 2.5 LayoutError — compute_layout Result döndürmeli // ============================================================================= #[test] fn test_2_5_compute_layout_returns_result() { // compute_layout artık Result dönüyor, unwrap panic yerine hata yönetimi let tpl = base_template(); let fonts = load_test_fonts(); let result: Result = compute_layout(&tpl, &serde_json::json!({}), &fonts); assert!(result.is_ok()); } // ============================================================================= // 2.7 FormatConfig — konfigürasyon bazlı para birimi formatlama // ============================================================================= #[test] fn test_2_7_format_config_default_turkish() { // Varsayılan: Türk Lirası formatı let formatted = dreport_layout::expr_eval::apply_format("18880", Some("currency")); assert_eq!(formatted, "18.880,00 ₺"); } #[test] fn test_2_7_format_config_custom() { // Özel config: USD formatı let config = FormatConfig { thousands_separator: ",".to_string(), decimal_separator: ".".to_string(), currency_symbol: "$".to_string(), currency_position: "prefix".to_string(), }; let formatted = dreport_layout::expr_eval::apply_format_with_config("18880", Some("currency"), &config); assert_eq!(formatted, "$18,880.00"); } #[test] fn test_2_7_format_config_number() { let config = FormatConfig { thousands_separator: " ".to_string(), decimal_separator: ",".to_string(), currency_symbol: "€".to_string(), currency_position: "suffix".to_string(), }; let formatted = dreport_layout::expr_eval::apply_format_with_config("1234567", Some("number"), &config); assert_eq!(formatted, "1 234 567"); } #[test] fn test_2_7_format_config_in_template() { // Template seviyesinde format_config ayarlanabilmeli let mut tpl = base_template(); tpl.format_config = Some(FormatConfig { thousands_separator: ",".to_string(), decimal_separator: ".".to_string(), currency_symbol: "$".to_string(), currency_position: "prefix".to_string(), }); // Serde ile serialize/deserialize çalışmalı let json = serde_json::to_string(&tpl).unwrap(); let parsed: Template = serde_json::from_str(&json).unwrap(); let fc = parsed.format_config.unwrap(); assert_eq!(fc.currency_symbol, "$"); assert_eq!(fc.thousands_separator, ","); } // ============================================================================= // Genel: Ellipse shape render // ============================================================================= #[test] fn test_ellipse_shape_renders() { let mut tpl = base_template(); tpl.root.children.push(TemplateElement::Shape(ShapeElement { id: "ellipse".to_string(), position: PositionMode::Flow, condition: None, size: SizeConstraint { width: SizeValue::Fixed { value: 40.0 }, height: SizeValue::Fixed { value: 20.0 }, ..Default::default() }, shape_type: "ellipse".to_string(), style: ContainerStyle { background_color: Some("#ff6600".to_string()), border_color: Some("#cc3300".to_string()), border_width: Some(0.5), ..Default::default() }, })); let fonts = load_test_fonts(); let layout = compute_layout(&tpl, &serde_json::json!({}), &fonts).unwrap(); let pdf = dreport_layout::pdf_render::render_pdf(&layout, &fonts).unwrap(); assert!(pdf.starts_with(b"%PDF")); } // ============================================================================= // 7.1 Conditional Rendering // ============================================================================= #[test] fn test_7_1_condition_gt_hides_element() { let mut tpl = base_template(); tpl.root.children.push(TemplateElement::StaticText(StaticTextElement { id: "always_visible".to_string(), condition: None, position: PositionMode::Flow, size: SizeConstraint::default(), style: TextStyle { font_size: Some(10.0), ..Default::default() }, content: "Visible".to_string(), })); tpl.root.children.push(TemplateElement::Text(TextElement { id: "conditional_text".to_string(), condition: Some(Condition { path: "toplamlar.iskonto".to_string(), operator: "gt".to_string(), value: Some(serde_json::json!(0)), }), position: PositionMode::Flow, size: SizeConstraint::default(), style: TextStyle { font_size: Some(10.0), ..Default::default() }, content: None, binding: ScalarBinding { path: "toplamlar.iskonto".to_string() }, })); let fonts = load_test_fonts(); // iskonto = 0 → koşul sağlanmaz, element gizlenmeli let data_no_iskonto = serde_json::json!({ "toplamlar": { "iskonto": 0 } }); let layout = compute_layout(&tpl, &data_no_iskonto, &fonts).unwrap(); let page = &layout.pages[0]; assert!( !page.elements.iter().any(|e| e.id == "conditional_text"), "iskonto=0 durumunda conditional_text gizlenmeli" ); assert!( page.elements.iter().any(|e| e.id == "always_visible"), "koşulsuz eleman her zaman görünmeli" ); } #[test] fn test_7_1_condition_gt_shows_element() { let mut tpl = base_template(); tpl.root.children.push(TemplateElement::Text(TextElement { id: "conditional_text".to_string(), condition: Some(Condition { path: "toplamlar.iskonto".to_string(), operator: "gt".to_string(), value: Some(serde_json::json!(0)), }), position: PositionMode::Flow, size: SizeConstraint::default(), style: TextStyle { font_size: Some(10.0), ..Default::default() }, content: None, binding: ScalarBinding { path: "toplamlar.iskonto".to_string() }, })); let fonts = load_test_fonts(); // iskonto = 500 → koşul sağlanır, element görünmeli let data_with_iskonto = serde_json::json!({ "toplamlar": { "iskonto": 500 } }); let layout = compute_layout(&tpl, &data_with_iskonto, &fonts).unwrap(); let page = &layout.pages[0]; assert!( page.elements.iter().any(|e| e.id == "conditional_text"), "iskonto>0 durumunda conditional_text görünmeli" ); } #[test] fn test_7_1_condition_eq_operator() { let mut tpl = base_template(); tpl.root.children.push(TemplateElement::StaticText(StaticTextElement { id: "status_text".to_string(), condition: Some(Condition { path: "durum".to_string(), operator: "eq".to_string(), value: Some(serde_json::json!("aktif")), }), position: PositionMode::Flow, size: SizeConstraint::default(), style: TextStyle { font_size: Some(10.0), ..Default::default() }, content: "Aktif".to_string(), })); let fonts = load_test_fonts(); // durum = "aktif" → görünür let layout = compute_layout(&tpl, &serde_json::json!({"durum": "aktif"}), &fonts).unwrap(); assert!(layout.pages[0].elements.iter().any(|e| e.id == "status_text")); // durum = "pasif" → gizli let layout = compute_layout(&tpl, &serde_json::json!({"durum": "pasif"}), &fonts).unwrap(); assert!(!layout.pages[0].elements.iter().any(|e| e.id == "status_text")); } #[test] fn test_7_1_condition_empty_not_empty() { let mut tpl = base_template(); tpl.root.children.push(TemplateElement::StaticText(StaticTextElement { id: "show_if_exists".to_string(), condition: Some(Condition { path: "note".to_string(), operator: "not_empty".to_string(), value: None, }), position: PositionMode::Flow, size: SizeConstraint::default(), style: TextStyle { font_size: Some(10.0), ..Default::default() }, content: "Has note".to_string(), })); let fonts = load_test_fonts(); // note yok → gizli let layout = compute_layout(&tpl, &serde_json::json!({}), &fonts).unwrap(); assert!(!layout.pages[0].elements.iter().any(|e| e.id == "show_if_exists")); // note var → görünür let layout = compute_layout(&tpl, &serde_json::json!({"note": "merhaba"}), &fonts).unwrap(); assert!(layout.pages[0].elements.iter().any(|e| e.id == "show_if_exists")); // note boş string → gizli let layout = compute_layout(&tpl, &serde_json::json!({"note": ""}), &fonts).unwrap(); assert!(!layout.pages[0].elements.iter().any(|e| e.id == "show_if_exists")); } #[test] fn test_7_1_condition_on_container_hides_children() { let mut tpl = base_template(); tpl.root.children.push(TemplateElement::Container(ContainerElement { id: "cond_container".to_string(), condition: Some(Condition { path: "show".to_string(), operator: "eq".to_string(), value: Some(serde_json::json!(true)), }), position: PositionMode::Flow, size: SizeConstraint::default(), direction: "column".to_string(), gap: 0.0, padding: Padding::default(), align: "stretch".to_string(), justify: "start".to_string(), style: ContainerStyle::default(), break_inside: "auto".to_string(), children: vec![TemplateElement::StaticText(StaticTextElement { id: "child_text".to_string(), condition: None, position: PositionMode::Flow, size: SizeConstraint::default(), style: TextStyle { font_size: Some(10.0), ..Default::default() }, content: "Child".to_string(), })], })); let fonts = load_test_fonts(); // show=false → container ve çocukları gizli let layout = compute_layout(&tpl, &serde_json::json!({"show": false}), &fonts).unwrap(); assert!(!layout.pages[0].elements.iter().any(|e| e.id == "cond_container")); assert!(!layout.pages[0].elements.iter().any(|e| e.id == "child_text")); // show=true → container ve çocukları görünür let layout = compute_layout(&tpl, &serde_json::json!({"show": true}), &fonts).unwrap(); assert!(layout.pages[0].elements.iter().any(|e| e.id == "cond_container")); assert!(layout.pages[0].elements.iter().any(|e| e.id == "child_text")); } // ============================================================================= // 7.5 Localization / FormatConfig from locale // ============================================================================= #[test] fn test_7_5_locale_en_us_currency() { let config = FormatConfig::from_locale("en-US"); assert_eq!(config.thousands_separator, ","); assert_eq!(config.decimal_separator, "."); assert_eq!(config.currency_symbol, "$"); assert_eq!(config.currency_position, "prefix"); } #[test] fn test_7_5_locale_de_de_currency() { let config = FormatConfig::from_locale("de-DE"); assert_eq!(config.thousands_separator, "."); assert_eq!(config.decimal_separator, ","); assert_eq!(config.currency_symbol, "€"); assert_eq!(config.currency_position, "suffix"); } #[test] fn test_7_5_locale_fr_fr_currency() { let config = FormatConfig::from_locale("fr-FR"); assert_eq!(config.thousands_separator, " "); assert_eq!(config.decimal_separator, ","); assert_eq!(config.currency_symbol, "€"); } #[test] fn test_7_5_locale_tr_default() { let config = FormatConfig::from_locale("tr-TR"); assert_eq!(config, FormatConfig::default()); } #[test] fn test_7_5_unknown_locale_falls_back_to_default() { let config = FormatConfig::from_locale("xx-XX"); assert_eq!(config, FormatConfig::default()); } #[test] fn test_7_5_effective_format_config_priority() { // format_config set → onu kullan let tpl = Template { id: "t1".to_string(), name: "Test".to_string(), page: PageSettings { width: 210.0, height: 297.0 }, fonts: vec![], header: None, footer: None, root: ContainerElement { id: "root".to_string(), condition: None, position: PositionMode::Flow, size: SizeConstraint::default(), direction: "column".to_string(), gap: 0.0, padding: Padding::default(), align: "stretch".to_string(), justify: "start".to_string(), style: ContainerStyle::default(), break_inside: "auto".to_string(), children: vec![], }, format_config: Some(FormatConfig { thousands_separator: ",".to_string(), decimal_separator: ".".to_string(), currency_symbol: "$".to_string(), currency_position: "prefix".to_string(), }), locale: Some("de-DE".to_string()), }; let fc = tpl.effective_format_config(); assert_eq!(fc.currency_symbol, "$"); // format_config kullanılır, de-DE değil } #[test] fn test_7_5_effective_format_config_locale_fallback() { let tpl = Template { id: "t1".to_string(), name: "Test".to_string(), page: PageSettings { width: 210.0, height: 297.0 }, fonts: vec![], header: None, footer: None, root: ContainerElement { id: "root".to_string(), condition: None, position: PositionMode::Flow, size: SizeConstraint::default(), direction: "column".to_string(), gap: 0.0, padding: Padding::default(), align: "stretch".to_string(), justify: "start".to_string(), style: ContainerStyle::default(), break_inside: "auto".to_string(), children: vec![], }, format_config: None, locale: Some("en-US".to_string()), }; let fc = tpl.effective_format_config(); assert_eq!(fc.currency_symbol, "$"); assert_eq!(fc.currency_position, "prefix"); } #[test] fn test_7_5_locale_affects_table_currency_format() { let mut tpl = base_template(); tpl.locale = Some("en-US".to_string()); tpl.root.children.push(TemplateElement::RepeatingTable(RepeatingTableElement { id: "tbl_locale".to_string(), condition: None, position: PositionMode::Flow, size: SizeConstraint { width: SizeValue::Fr { value: 1.0 }, height: SizeValue::Auto, ..Default::default() }, data_source: ArrayBinding { path: "items".to_string() }, columns: vec![ TableColumn { id: "col_price".to_string(), field: "price".to_string(), title: "Price".to_string(), width: SizeValue::Fr { value: 1.0 }, align: "right".to_string(), format: Some("currency".to_string()), }, ], style: TableStyle::default(), repeat_header: Some(true), })); let data = serde_json::json!({ "items": [ { "price": 1500 } ] }); // data_resolve seviyesinde kontrol: locale en-US → $ prefix, comma thousands let resolved = dreport_layout::data_resolve::resolve_template(&tpl, &data); let table = resolved.tables.get("tbl_locale").expect("tbl_locale should be resolved"); assert_eq!(table.rows.len(), 1); assert_eq!(table.rows[0][0], "$1,500.00"); } // ============================================================================= // 8.1 Chart Legend — tek seri durumunda da render edilmeli // ============================================================================= #[test] fn test_8_1_legend_renders_for_single_series() { use dreport_layout::chart_render::render_svg; use dreport_layout::data_resolve::{ChartSeries, ResolvedChartData}; let data = ResolvedChartData { chart_type: ChartType::Bar, categories: vec!["A".to_string(), "B".to_string()], series: vec![ChartSeries { name: "Revenue".to_string(), values: vec![100.0, 200.0], }], title: None, legend: Some(ChartLegend { show: true, position: None, font_size: None }), labels: None, axis: None, style: ChartStyle::default(), group_mode: None, }; let svg = render_svg(&data, 100.0, 60.0); let has_swatch = svg.contains(r#"width="2.5" height="2.5""#); assert!(has_swatch, "tek serili chart'ta legend.show=true olunca legend render edilmeli"); assert!(svg.contains("Revenue"), "legend seri adını göstermeli"); } #[test] fn test_8_1_legend_hidden_when_show_false() { use dreport_layout::chart_render::render_svg; use dreport_layout::data_resolve::{ChartSeries, ResolvedChartData}; let data = ResolvedChartData { chart_type: ChartType::Bar, categories: vec!["A".to_string()], series: vec![ChartSeries { name: "Sales".to_string(), values: vec![50.0], }], title: None, legend: Some(ChartLegend { show: false, position: None, font_size: None }), labels: None, axis: None, style: ChartStyle::default(), group_mode: None, }; let svg = render_svg(&data, 100.0, 60.0); let has_swatch = svg.contains(r#"width="2.5" height="2.5""#); assert!(!has_swatch, "legend.show=false olunca legend render edilmemeli"); } // ============================================================================= // 8.2 Pie Chart Label Kontrolü // ============================================================================= #[test] fn test_8_2_pie_labels_hidden_when_show_false() { use dreport_layout::chart_render::render_svg; use dreport_layout::data_resolve::{ChartSeries, ResolvedChartData}; let data = ResolvedChartData { chart_type: ChartType::Pie, categories: vec!["Gida".to_string(), "Ulasim".to_string(), "Kira".to_string()], series: vec![ChartSeries { name: "data".to_string(), values: vec![50.0, 30.0, 20.0], }], title: None, legend: None, labels: Some(ChartLabels { show: false, font_size: None, color: None }), axis: None, style: ChartStyle::default(), group_mode: None, }; let svg = render_svg(&data, 80.0, 80.0); assert!(!svg.contains("Gida"), "labels.show=false iken kategori adı görünmemeli"); assert!(!svg.contains("Ulasim"), "labels.show=false iken kategori adı görünmemeli"); assert!(!svg.contains("50%"), "labels.show=false iken yüzde etiketi görünmemeli"); } #[test] fn test_8_2_pie_labels_shown_when_show_true() { use dreport_layout::chart_render::render_svg; use dreport_layout::data_resolve::{ChartSeries, ResolvedChartData}; let data = ResolvedChartData { chart_type: ChartType::Pie, categories: vec!["Gida".to_string(), "Ulasim".to_string()], series: vec![ChartSeries { name: "data".to_string(), values: vec![75.0, 25.0], }], title: None, legend: None, labels: Some(ChartLabels { show: true, font_size: None, color: None }), axis: None, style: ChartStyle::default(), group_mode: None, }; let svg = render_svg(&data, 80.0, 80.0); assert!(svg.contains("Gida"), "labels.show=true iken kategori adı görünmeli"); assert!(svg.contains("75%"), "labels.show=true iken yüzde etiketi görünmeli"); }