mirror of
https://github.com/duhanbalci/dreport.git
synced 2026-07-01 18:39:16 +00:00
refactor & improvements
This commit is contained in:
321
layout-engine/tests/layout_integration.rs
Normal file
321
layout-engine/tests/layout_integration.rs
Normal file
@@ -0,0 +1,321 @@
|
||||
//! Integration tests for the layout engine's compute_layout() public API.
|
||||
|
||||
use dreport_core::models::*;
|
||||
use dreport_layout::{compute_layout, FontData, LayoutResult};
|
||||
|
||||
fn load_test_fonts() -> Vec<FontData> {
|
||||
let font_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("backend/fonts");
|
||||
|
||||
let mut fonts = Vec::new();
|
||||
for entry in std::fs::read_dir(&font_dir).expect("backend/fonts directory not found") {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
if path.extension().is_some_and(|e| e == "ttf") {
|
||||
let family = path
|
||||
.file_stem()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.split('-')
|
||||
.next()
|
||||
.unwrap_or("Unknown")
|
||||
.to_string();
|
||||
// Map NotoSans → "Noto Sans", NotoSansMono → "Noto Sans Mono"
|
||||
let family = if family == "NotoSansMono" {
|
||||
"Noto Sans Mono".to_string()
|
||||
} else if family == "NotoSans" {
|
||||
"Noto Sans".to_string()
|
||||
} else {
|
||||
family
|
||||
};
|
||||
fonts.push(FontData {
|
||||
family,
|
||||
data: std::fs::read(&path).unwrap(),
|
||||
});
|
||||
}
|
||||
}
|
||||
fonts
|
||||
}
|
||||
|
||||
fn simple_template() -> Template {
|
||||
Template {
|
||||
id: "test".to_string(),
|
||||
name: "Test".to_string(),
|
||||
page: PageSettings {
|
||||
width: 210.0,
|
||||
height: 297.0,
|
||||
},
|
||||
fonts: vec!["Noto Sans".to_string()],
|
||||
root: ContainerElement {
|
||||
id: "root".to_string(),
|
||||
position: PositionMode::Flow,
|
||||
size: SizeConstraint::default(),
|
||||
direction: "column".to_string(),
|
||||
gap: 5.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(),
|
||||
children: vec![TemplateElement::StaticText(StaticTextElement {
|
||||
id: "title".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()),
|
||||
..Default::default()
|
||||
},
|
||||
content: "Hello World".to_string(),
|
||||
})],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_layout_single_page() {
|
||||
let template = simple_template();
|
||||
let data = serde_json::json!({});
|
||||
let fonts = load_test_fonts();
|
||||
|
||||
let result: LayoutResult = compute_layout(&template, &data, &fonts);
|
||||
|
||||
assert_eq!(result.pages.len(), 1);
|
||||
let page = &result.pages[0];
|
||||
assert_eq!(page.width_mm, 210.0);
|
||||
assert_eq!(page.height_mm, 297.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_layout_elements_within_page() {
|
||||
let template = simple_template();
|
||||
let data = serde_json::json!({});
|
||||
let fonts = load_test_fonts();
|
||||
|
||||
let result = compute_layout(&template, &data, &fonts);
|
||||
let page = &result.pages[0];
|
||||
|
||||
// Should have at least root + title = 2 elements
|
||||
assert!(
|
||||
page.elements.len() >= 2,
|
||||
"Expected at least 2 elements, got {}",
|
||||
page.elements.len()
|
||||
);
|
||||
|
||||
for el in &page.elements {
|
||||
// All positions should be non-negative
|
||||
assert!(
|
||||
el.x_mm >= 0.0,
|
||||
"Element {} has negative x: {}",
|
||||
el.id,
|
||||
el.x_mm
|
||||
);
|
||||
assert!(
|
||||
el.y_mm >= 0.0,
|
||||
"Element {} has negative y: {}",
|
||||
el.id,
|
||||
el.y_mm
|
||||
);
|
||||
// All dimensions should be non-negative
|
||||
assert!(
|
||||
el.width_mm >= 0.0,
|
||||
"Element {} has negative width: {}",
|
||||
el.id,
|
||||
el.width_mm
|
||||
);
|
||||
assert!(
|
||||
el.height_mm >= 0.0,
|
||||
"Element {} has negative height: {}",
|
||||
el.id,
|
||||
el.height_mm
|
||||
);
|
||||
// Elements should be within page bounds (with small tolerance for rounding)
|
||||
assert!(
|
||||
el.x_mm + el.width_mm <= page.width_mm + 1.0,
|
||||
"Element {} exceeds page width: x={}+w={} > {}",
|
||||
el.id,
|
||||
el.x_mm,
|
||||
el.width_mm,
|
||||
page.width_mm
|
||||
);
|
||||
assert!(
|
||||
el.y_mm + el.height_mm <= page.height_mm + 1.0,
|
||||
"Element {} exceeds page height: y={}+h={} > {}",
|
||||
el.id,
|
||||
el.y_mm,
|
||||
el.height_mm,
|
||||
page.height_mm
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_layout_text_content_resolved() {
|
||||
let template = simple_template();
|
||||
let data = serde_json::json!({});
|
||||
let fonts = load_test_fonts();
|
||||
|
||||
let result = compute_layout(&template, &data, &fonts);
|
||||
let page = &result.pages[0];
|
||||
|
||||
let title = page.elements.iter().find(|e| e.id == "title").unwrap();
|
||||
match &title.content {
|
||||
Some(dreport_layout::ResolvedContent::Text { value }) => {
|
||||
assert_eq!(value, "Hello World");
|
||||
}
|
||||
other => panic!("Expected Text content, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_layout_with_data_binding() {
|
||||
let template = Template {
|
||||
id: "t1".to_string(),
|
||||
name: "Binding Test".to_string(),
|
||||
page: PageSettings {
|
||||
width: 210.0,
|
||||
height: 297.0,
|
||||
},
|
||||
fonts: vec!["Noto Sans".to_string()],
|
||||
root: ContainerElement {
|
||||
id: "root".to_string(),
|
||||
position: PositionMode::Flow,
|
||||
size: SizeConstraint::default(),
|
||||
direction: "column".to_string(),
|
||||
gap: 0.0,
|
||||
padding: Padding {
|
||||
top: 10.0,
|
||||
right: 10.0,
|
||||
bottom: 10.0,
|
||||
left: 10.0,
|
||||
},
|
||||
align: "stretch".to_string(),
|
||||
justify: "start".to_string(),
|
||||
style: ContainerStyle::default(),
|
||||
children: vec![TemplateElement::Text(TextElement {
|
||||
id: "bound_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),
|
||||
..Default::default()
|
||||
},
|
||||
content: None,
|
||||
binding: ScalarBinding {
|
||||
path: "company.name".to_string(),
|
||||
},
|
||||
})],
|
||||
},
|
||||
};
|
||||
|
||||
let data = serde_json::json!({
|
||||
"company": { "name": "Acme Corp" }
|
||||
});
|
||||
let fonts = load_test_fonts();
|
||||
|
||||
let result = compute_layout(&template, &data, &fonts);
|
||||
let page = &result.pages[0];
|
||||
|
||||
let bound = page
|
||||
.elements
|
||||
.iter()
|
||||
.find(|e| e.id == "bound_text")
|
||||
.unwrap();
|
||||
match &bound.content {
|
||||
Some(dreport_layout::ResolvedContent::Text { value }) => {
|
||||
assert_eq!(value, "Acme Corp");
|
||||
}
|
||||
other => panic!("Expected Text content, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_layout_multiple_children_ordering() {
|
||||
let template = Template {
|
||||
id: "t1".to_string(),
|
||||
name: "Order Test".to_string(),
|
||||
page: PageSettings {
|
||||
width: 210.0,
|
||||
height: 297.0,
|
||||
},
|
||||
fonts: vec!["Noto Sans".to_string()],
|
||||
root: ContainerElement {
|
||||
id: "root".to_string(),
|
||||
position: PositionMode::Flow,
|
||||
size: SizeConstraint::default(),
|
||||
direction: "column".to_string(),
|
||||
gap: 5.0,
|
||||
padding: Padding {
|
||||
top: 10.0,
|
||||
right: 10.0,
|
||||
bottom: 10.0,
|
||||
left: 10.0,
|
||||
},
|
||||
align: "stretch".to_string(),
|
||||
justify: "start".to_string(),
|
||||
style: ContainerStyle::default(),
|
||||
children: vec![
|
||||
TemplateElement::StaticText(StaticTextElement {
|
||||
id: "first".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: "First".to_string(),
|
||||
}),
|
||||
TemplateElement::StaticText(StaticTextElement {
|
||||
id: "second".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: "Second".to_string(),
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
let data = serde_json::json!({});
|
||||
let fonts = load_test_fonts();
|
||||
|
||||
let result = compute_layout(&template, &data, &fonts);
|
||||
let page = &result.pages[0];
|
||||
|
||||
let first = page.elements.iter().find(|e| e.id == "first").unwrap();
|
||||
let second = page.elements.iter().find(|e| e.id == "second").unwrap();
|
||||
|
||||
// In column direction, second should be below first
|
||||
assert!(
|
||||
second.y_mm > first.y_mm,
|
||||
"Second element (y={}) should be below first (y={})",
|
||||
second.y_mm,
|
||||
first.y_mm
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user