fmt
Some checks failed
CI / rust (push) Successful in 47s
CI / frontend (push) Failing after 10s
CI / wasm (push) Successful in 1m45s
CI / publish-crates (push) Successful in 23s
CI / publish-npm (push) Has been skipped

This commit is contained in:
2026-04-07 01:45:38 +03:00
parent b6aecc5f12
commit 603624724c
28 changed files with 1674 additions and 784 deletions

View File

@@ -8,7 +8,7 @@
#![cfg(not(target_arch = "wasm32"))]
use dreport_core::models::*;
use dreport_layout::{compute_layout, LayoutResult, ResolvedContent};
use dreport_layout::{LayoutResult, ResolvedContent, compute_layout};
mod common;
use common::load_test_fonts;
@@ -17,7 +17,10 @@ fn base_template() -> Template {
Template {
id: "imp_test".to_string(),
name: "Improvements Test".to_string(),
page: PageSettings { width: 210.0, height: 297.0 },
page: PageSettings {
width: 210.0,
height: 297.0,
},
fonts: vec!["Noto Sans".to_string()],
header: None,
footer: None,
@@ -28,7 +31,12 @@ fn base_template() -> Template {
size: SizeConstraint::default(),
direction: "column".to_string(),
gap: 5.0,
padding: Padding { top: 15.0, right: 15.0, bottom: 15.0, left: 15.0 },
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(),
@@ -63,7 +71,11 @@ fn test_1_2_text_wrapping_layout_height() {
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();
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ı.
@@ -125,7 +137,11 @@ fn test_1_3_image_object_fit_in_layout() {
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();
let el = result.pages[0]
.elements
.iter()
.find(|e| e.id == "img_contain")
.unwrap();
// objectFit style'da taşınmalı
assert_eq!(
@@ -143,27 +159,33 @@ fn test_1_3_image_object_fit_in_layout() {
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,
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(),
}));
tpl.root
.children
.push(TemplateElement::StaticText(StaticTextElement {
id: "italic_text".to_string(),
position: PositionMode::Flow,
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();
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ı
@@ -174,22 +196,24 @@ fn test_1_4_italic_font_in_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,
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(),
}));
tpl.root
.children
.push(TemplateElement::StaticText(StaticTextElement {
id: "bold_italic".to_string(),
position: PositionMode::Flow,
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();
@@ -205,28 +229,30 @@ fn test_1_4_bold_italic_font_in_pdf() {
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,
size: SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
..Default::default()
},
data_source: ArrayBinding { path: "items".to_string() },
columns: vec![
TableColumn {
tpl.root
.children
.push(TemplateElement::RepeatingTable(RepeatingTableElement {
id: "tbl_no_repeat".to_string(),
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_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
}));
}],
style: TableStyle::default(),
repeat_header: Some(false), // Header tekrarlanmasın
}));
// Çok sayıda satır — sayfa taşması için
let items: Vec<serde_json::Value> = (0..80)
@@ -253,9 +279,9 @@ fn test_2_1_repeat_header_false_no_repeat_on_second_page() {
.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")
});
let has_header_clone = page2_ids
.iter()
.any(|id| id.contains("header") && id.contains("tbl_no_repeat") && id.contains("_p"));
assert!(
!has_header_clone,
@@ -268,28 +294,30 @@ fn test_2_1_repeat_header_false_no_repeat_on_second_page() {
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,
size: SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
height: SizeValue::Auto,
..Default::default()
},
data_source: ArrayBinding { path: "items".to_string() },
columns: vec![
TableColumn {
tpl.root
.children
.push(TemplateElement::RepeatingTable(RepeatingTableElement {
id: "tbl_repeat".to_string(),
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_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),
}));
}],
style: TableStyle::default(),
repeat_header: Some(true),
}));
let items: Vec<serde_json::Value> = (0..80)
.map(|i| serde_json::json!({ "name": format!("Item {}", i) }))
@@ -308,9 +336,9 @@ fn test_2_1_repeat_header_true_repeats_on_second_page() {
.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")
});
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
@@ -320,10 +348,17 @@ fn test_2_1_repeat_header_true_repeats_on_second_page() {
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"));
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);
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,
@@ -342,36 +377,40 @@ fn test_2_1_repeat_header_true_repeats_on_second_page() {
#[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,
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(),
tpl.root
.children
.push(TemplateElement::RepeatingTable(RepeatingTableElement {
id: "tbl_fmt".to_string(),
position: PositionMode::Flow,
size: SizeConstraint {
width: SizeValue::Fr { value: 1.0 },
align: "left".to_string(),
format: None,
height: SizeValue::Auto,
..Default::default()
},
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()),
data_source: ArrayBinding {
path: "items".to_string(),
},
],
style: TableStyle::default(),
repeat_header: Some(true),
}));
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": [
@@ -431,7 +470,11 @@ fn test_2_3_rounded_rectangle_renders() {
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();
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));
@@ -448,17 +491,22 @@ fn test_2_3_container_border_radius_renders() {
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,
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(),
}));
tpl.root
.children
.push(TemplateElement::StaticText(StaticTextElement {
id: "text_in_rounded".to_string(),
position: PositionMode::Flow,
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();
@@ -499,7 +547,8 @@ fn test_2_7_format_config_custom() {
currency_symbol: "$".to_string(),
currency_position: "prefix".to_string(),
};
let formatted = dreport_layout::expr_eval::apply_format_with_config("18880", Some("currency"), &config);
let formatted =
dreport_layout::expr_eval::apply_format_with_config("18880", Some("currency"), &config);
assert_eq!(formatted, "$18,880.00");
}
@@ -511,7 +560,8 @@ fn test_2_7_format_config_number() {
currency_symbol: "".to_string(),
currency_position: "suffix".to_string(),
};
let formatted = dreport_layout::expr_eval::apply_format_with_config("1234567", Some("number"), &config);
let formatted =
dreport_layout::expr_eval::apply_format_with_config("1234567", Some("number"), &config);
assert_eq!(formatted, "1 234 567");
}