mod common;
use common::{SceneCommand, compile, default_provider, parse};
fn table_src() -> &'static str {
r##"zenith version=1 {
project id="proj.tbl" name="TBL"
tokens format="zenith-token-v1" {
token id="color.line" type="color" value="#cccccc"
token id="color.cellbg" type="color" value="#f0f0f0"
token id="color.ink" type="color" value="#000000"
}
styles {}
document id="doc.tbl" title="TBL" {
page id="page.tbl" w=(px)640 h=(px)400 {
table id="t1" x=(px)40 y=(px)40 w=(px)520 h=(px)240 border=(token)"color.line" border-width=(px)1 fill=(token)"color.cellbg" cell-padding=(px)0 gap=(px)0 {
column width=(px)160
column
column
row {
cell { text id="c11" x=(px)0 y=(px)0 fill=(token)"color.ink" { span "Name" } }
cell colspan=2 { text id="c12" x=(px)0 y=(px)0 fill=(token)"color.ink" { span "Details" } }
}
row {
cell { text id="c21" x=(px)0 y=(px)0 fill=(token)"color.ink" { span "Ada" } }
cell { text id="c22" x=(px)0 y=(px)0 fill=(token)"color.ink" { span "Lovelace" } }
cell { text id="c23" x=(px)0 y=(px)0 fill=(token)"color.ink" { span "1815" } }
}
}
}
}
}
"##
}
#[test]
fn table_emits_cell_backgrounds_and_borders() {
let doc = parse(table_src());
let result = compile(&doc, &default_provider());
let fill_count = result
.scene
.commands
.iter()
.filter(|c| matches!(c, SceneCommand::FillRect { .. }))
.count();
let stroke_count = result
.scene
.commands
.iter()
.filter(|c| matches!(c, SceneCommand::StrokeLine { .. }))
.count();
assert_eq!(fill_count, 5, "expected one fill per placed cell");
assert_eq!(
stroke_count,
5 * 4,
"expected four border edges per placed cell"
);
let glyph_runs = result
.scene
.commands
.iter()
.filter(|c| matches!(c, SceneCommand::DrawGlyphRun { .. }))
.count();
assert_eq!(glyph_runs, 5, "expected one glyph run per cell text");
}
#[test]
fn colspan_cell_spans_two_columns() {
let doc = parse(table_src());
let result = compile(&doc, &default_provider());
let fills: Vec<(f64, f64)> = result
.scene
.commands
.iter()
.filter_map(|c| match c {
SceneCommand::FillRect { x, w, .. } => Some((*x, *w)),
_ => None,
})
.collect();
assert_eq!(fills.len(), 5, "expected 5 cell fills; got {fills:?}");
assert!((fills[0].0 - 40.0).abs() < 0.01, "cell0 x; got {fills:?}");
assert!((fills[0].1 - 160.0).abs() < 0.01, "cell0 w; got {fills:?}");
assert!(
(fills[1].0 - 200.0).abs() < 0.01,
"colspan x; got {fills:?}"
);
let col1_w = fills[3].1;
let col2_w = fills[4].1;
assert!(
(fills[1].1 - (col1_w + col2_w)).abs() < 0.5,
"colspan w must span both auto columns: {} vs {}+{}; got {fills:?}",
fills[1].1,
col1_w,
col2_w
);
assert!(
(fills[3].0 - 200.0).abs() < 0.01,
"row2 col1 x; got {fills:?}"
);
assert!(
(fills[4].0 - (200.0 + col1_w)).abs() < 0.5,
"row2 col2 x; got {fills:?}"
);
assert!(
col1_w > 0.0 && col2_w > 0.0,
"auto cols sized; got {fills:?}"
);
}
#[test]
fn auto_column_sizes_to_widest_text() {
let src = r##"zenith version=1 {
project id="proj.aw" name="AW"
tokens format="zenith-token-v1" {
token id="color.ink" type="color" value="#000000"
}
styles {}
document id="doc.aw" title="AW" {
page id="page.aw" w=(px)800 h=(px)400 {
table id="t.aw" x=(px)0 y=(px)0 w=(px)800 h=(px)300 fill=(token)"color.ink" cell-padding=(px)0 gap=(px)0 {
column
column
row {
cell { text id="a1" x=(px)0 y=(px)0 { span "Hi" } }
cell { text id="a2" x=(px)0 y=(px)0 { span "Supercalifragilistic" } }
}
row {
cell { text id="b1" x=(px)0 y=(px)0 { span "Ok" } }
cell { text id="b2" x=(px)0 y=(px)0 { span "Antidisestablishmentarianism" } }
}
}
}
}
}
"##;
let doc = parse(src);
let result = compile(&doc, &default_provider());
let fills: Vec<(f64, f64)> = result
.scene
.commands
.iter()
.filter_map(|c| match c {
SceneCommand::FillRect { x, w, .. } => Some((*x, *w)),
_ => None,
})
.collect();
assert_eq!(fills.len(), 4, "expected 4 cell fills; got {fills:?}");
let col0_w = fills[0].1;
let col1_w = fills[1].1;
assert!(
col1_w > col0_w,
"the long-text column must be wider than the short-text column: {col1_w} vs {col0_w}"
);
}
#[test]
fn wrapping_text_makes_row_taller() {
let src = r##"zenith version=1 {
project id="proj.rh" name="RH"
tokens format="zenith-token-v1" {
token id="color.ink" type="color" value="#000000"
}
styles {}
document id="doc.rh" title="RH" {
page id="page.rh" w=(px)400 h=(px)600 {
table id="t.rh" x=(px)0 y=(px)0 w=(px)200 h=(px)500 fill=(token)"color.ink" cell-padding=(px)0 gap=(px)0 {
column width=(px)40
column width=(px)80
row {
cell { text id="r0a" x=(px)0 y=(px)0 { span "A" } }
cell { text id="r0b" x=(px)0 y=(px)0 w=(px)80 { span "Short" } }
}
row {
cell { text id="r1a" x=(px)0 y=(px)0 { span "B" } }
cell { text id="r1b" x=(px)0 y=(px)0 w=(px)80 { span "alpha bravo charlie delta echo foxtrot golf hotel india juliet" } }
}
}
}
}
}
"##;
let doc = parse(src);
let result = compile(&doc, &default_provider());
let fills: Vec<(f64, f64)> = result
.scene
.commands
.iter()
.filter_map(|c| match c {
SceneCommand::FillRect { y, h, .. } => Some((*y, *h)),
_ => None,
})
.collect();
assert_eq!(fills.len(), 4, "expected 4 cell fills; got {fills:?}");
let row0_h = fills[0].1;
let row1_h = fills[2].1;
assert!(
row1_h > row0_h + 1.0,
"the wrapping row must be taller than the single-line row: {row1_h} vs {row0_h}"
);
assert!(
fills[2].0 > fills[0].0,
"row 1 must sit below row 0; got {fills:?}"
);
}
#[test]
fn all_explicit_columns_unchanged() {
let src = r##"zenith version=1 {
project id="proj.ex" name="EX"
tokens format="zenith-token-v1" {
token id="color.ink" type="color" value="#000000"
}
styles {}
document id="doc.ex" title="EX" {
page id="page.ex" w=(px)800 h=(px)400 {
table id="t.ex" x=(px)10 y=(px)10 w=(px)600 h=(px)300 fill=(token)"color.ink" cell-padding=(px)0 gap=(px)0 {
column width=(px)100
column width=(px)250
column width=(px)90
row {
cell { text id="e1" x=(px)0 y=(px)0 { span "One" } }
cell { text id="e2" x=(px)0 y=(px)0 { span "Two" } }
cell { text id="e3" x=(px)0 y=(px)0 { span "Three" } }
}
}
}
}
}
"##;
let doc = parse(src);
let result = compile(&doc, &default_provider());
let fills: Vec<(f64, f64)> = result
.scene
.commands
.iter()
.filter_map(|c| match c {
SceneCommand::FillRect { x, w, .. } => Some((*x, *w)),
_ => None,
})
.collect();
assert_eq!(fills.len(), 3, "expected 3 cell fills; got {fills:?}");
assert!((fills[0].0 - 10.0).abs() < 0.01, "col0 x; got {fills:?}");
assert!((fills[0].1 - 100.0).abs() < 0.01, "col0 w; got {fills:?}");
assert!((fills[1].0 - 110.0).abs() < 0.01, "col1 x; got {fills:?}");
assert!((fills[1].1 - 250.0).abs() < 0.01, "col1 w; got {fills:?}");
assert!((fills[2].0 - 360.0).abs() < 0.01, "col2 x; got {fills:?}");
assert!((fills[2].1 - 90.0).abs() < 0.01, "col2 w; got {fills:?}");
}
#[test]
fn cell_text_positioned_at_content_origin() {
let doc = parse(table_src());
let result = compile(&doc, &default_provider());
let first_run_x = result.scene.commands.iter().find_map(|c| match c {
SceneCommand::DrawGlyphRun { x, .. } => Some(*x),
_ => None,
});
assert_eq!(
first_run_x,
Some(40.0),
"first cell text must sit at the cell content origin x=40"
);
}
#[test]
fn invisible_table_emits_nothing() {
let src = table_src().replace("table id=\"t1\"", "table id=\"t1\" visible=#false");
let doc = parse(&src);
let result = compile(&doc, &default_provider());
let drawn = result.scene.commands.iter().any(|c| {
matches!(
c,
SceneCommand::FillRect { .. }
| SceneCommand::StrokeLine { .. }
| SceneCommand::DrawGlyphRun { .. }
)
});
assert!(!drawn, "an invisible table must emit no drawing commands");
}
#[test]
fn separate_mode_stroke_count_unchanged() {
let result = compile(&parse(table_src()), &default_provider());
let stroke_count = result
.scene
.commands
.iter()
.filter(|c| matches!(c, SceneCommand::StrokeLine { .. }))
.count();
assert_eq!(
stroke_count,
5 * 4,
"separate mode (default) must emit 4 border edges per placed cell; got {stroke_count}"
);
}
#[test]
fn non_flow_table_command_count_unchanged() {
let result = compile(&parse(table_src()), &default_provider());
let total = result.scene.commands.len();
let flow_pass_added = result
.scene
.commands
.iter()
.any(|c| matches!(c, SceneCommand::DrawGlyphRun { .. }));
assert!(total > 0 && flow_pass_added, "non-flow table still renders");
}