234 lines
11 KiB
Rust
234 lines
11 KiB
Rust
use super::*;
|
|
use crate::resolve::Ctx;
|
|
|
|
#[test]
|
|
fn new_presentation_is_valid() {
|
|
let p = Presentation::default();
|
|
assert_eq!(p.slides.len(), 1);
|
|
assert_eq!(p.masters.len(), 1);
|
|
assert_eq!(p.masters[0].layouts.len(), 11);
|
|
assert!(p.validate().is_empty(), "{:?}", p.validate());
|
|
let s = &p.slides[0];
|
|
let (m, l) = p.master_for(s).unwrap();
|
|
assert_eq!(l.unwrap().kind, LayoutType::Title);
|
|
assert_eq!(m.theme.name, "DeckCraft");
|
|
// Title slide has a centred title and a subtitle.
|
|
let kinds: Vec<_> = s.shapes.iter().filter_map(|x| x.ph_type()).collect();
|
|
assert_eq!(kinds, vec![PhType::CtrTitle, PhType::SubTitle]);
|
|
}
|
|
|
|
#[test]
|
|
fn placeholder_inherits_position_and_styles() {
|
|
let p = Presentation::default();
|
|
let s = &p.slides[0];
|
|
let ctx = Ctx::for_slide(&p, s).unwrap();
|
|
let title = &s.shapes[0];
|
|
let x = resolve::xfrm(&ctx, title);
|
|
assert!((x.x - 120.0).abs() < 1e-9 && (x.w - 720.0).abs() < 1e-9);
|
|
let lvl = resolve::level_style(&ctx, title, 0);
|
|
assert_eq!(lvl.run.size, Some(60.0)); // layout overrides the master's 44
|
|
assert_eq!(lvl.para.align, Some(text::Align::Center));
|
|
assert_eq!(resolve::font_family(&ctx, &lvl.run), "Inter");
|
|
let body = resolve::body(&ctx, title);
|
|
assert_eq!(body.anchor, Some(text::Anchor::Bottom));
|
|
}
|
|
|
|
#[test]
|
|
fn body_levels_from_master() {
|
|
let mut p = Presentation::default();
|
|
let lay = defaults::layout_of_kind(&p, LayoutType::TitleAndContent).unwrap();
|
|
let s = defaults::new_slide(&mut p, lay);
|
|
let ctx = Ctx::for_slide(&p, &s).unwrap();
|
|
let body = &s.shapes[1];
|
|
assert_eq!(body.ph_type(), Some(PhType::Obj));
|
|
let l0 = resolve::level_style(&ctx, body, 0);
|
|
let l1 = resolve::level_style(&ctx, body, 1);
|
|
assert_eq!(l0.run.size, Some(28.0));
|
|
assert_eq!(l1.run.size, Some(24.0));
|
|
assert!(matches!(l0.para.bullet, Some(text::Bullet::Char { .. })));
|
|
}
|
|
|
|
#[test]
|
|
fn shape_style_fill_resolves_through_theme() {
|
|
let p = Presentation::default();
|
|
let s = &p.slides[0];
|
|
let ctx = Ctx::for_slide(&p, s).unwrap();
|
|
let sh = Shape { style: Some(ShapeStyle::accent(SchemeSlot::Accent1)), xfrm: Some(Xfrm::new(0.0, 0.0, 10.0, 10.0)), ..Default::default() };
|
|
let (f, ph) = resolve::fill(&ctx, &sh);
|
|
let Some(Fill::Solid { color }) = f else { panic!("expected solid") };
|
|
assert_eq!(ctx.color(&color, ph), p.masters[0].theme.colors.get(SchemeSlot::Accent1));
|
|
let (l, ph) = resolve::line(&ctx, &sh);
|
|
assert!(l.width.is_some());
|
|
assert!(ph.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn a_gradient_without_stops_takes_them_from_its_style() {
|
|
// `a:gradFill` with only a direction: PowerPoint fills it with the stops of the theme gradient
|
|
// the style's fillRef names, in the style's colour, along the shape's own direction.
|
|
let p = Presentation::default();
|
|
let s = &p.slides[0];
|
|
let ctx = Ctx::for_slide(&p, s).unwrap();
|
|
let own =
|
|
style::Gradient { tile: [0.0; 4], stops: vec![], shape: style::GradientShape::Linear { angle: 45.0, scaled: true }, rotate_with_shape: true };
|
|
let mut st = ShapeStyle::accent(SchemeSlot::Accent2);
|
|
st.fill_ref.0 = 3;
|
|
let sh = Shape { style: Some(st.clone()), fill: Some(Fill::Gradient(own.clone())), ..Default::default() };
|
|
let (f, ph) = resolve::fill(&ctx, &sh);
|
|
let Some(Fill::Gradient(g)) = f else { panic!("expected a gradient") };
|
|
let Some(Fill::Gradient(theme)) = p.masters[0].theme.format.fills.get(2).cloned() else { panic!("theme fill 3 is a gradient") };
|
|
assert_eq!(g.stops, theme.stops);
|
|
assert_eq!(g.shape, own.shape, "the shape's own direction");
|
|
assert_eq!(ph, Some(p.masters[0].theme.colors.get(SchemeSlot::Accent2)));
|
|
// A solid style fill has no stops to give: the gradient stays empty (draws nothing).
|
|
st.fill_ref.0 = 1;
|
|
let sh = Shape { style: Some(st), fill: Some(Fill::Gradient(own.clone())), ..Default::default() };
|
|
assert_eq!(resolve::fill(&ctx, &sh).0, Some(Fill::Gradient(own)));
|
|
}
|
|
|
|
#[test]
|
|
fn serde_roundtrip() {
|
|
let p = Presentation::default();
|
|
let j = serde_json::to_string(&p).unwrap();
|
|
let q: Presentation = serde_json::from_str(&j).unwrap();
|
|
assert_eq!(p, q);
|
|
// Unknown and missing fields are tolerated.
|
|
let q: Presentation = serde_json::from_str(r#"{"slides":[],"zzz":1}"#).unwrap();
|
|
assert!(q.slides.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn ids_are_unique_and_fix_next_id() {
|
|
let mut p = Presentation::default();
|
|
let a = p.alloc_id();
|
|
let b = p.alloc_id();
|
|
assert!(b > a);
|
|
p.next_id = 0;
|
|
p.fix_next_id();
|
|
assert!(p.next_id >= b.min(p.next_id));
|
|
let c = p.alloc_id();
|
|
let mut all = vec![];
|
|
for s in &p.slides {
|
|
all.push(s.id.0);
|
|
walk(&s.shapes, &mut |sh, _| all.push(sh.id.0));
|
|
}
|
|
assert!(!all.contains(&c));
|
|
}
|
|
|
|
#[test]
|
|
fn group_find_and_walk() {
|
|
let child = Shape { id: ShapeId(900), ..Default::default() };
|
|
let g = Shape { id: ShapeId(901), kind: ShapeKind::Group { children: vec![child], child: Xfrm::new(0.0, 0.0, 1.0, 1.0) }, ..Default::default() };
|
|
let mut slide = Slide { shapes: vec![g], ..Default::default() };
|
|
assert!(slide.shape(ShapeId(900)).is_some());
|
|
assert!(slide.shape_mut(ShapeId(900)).is_some());
|
|
let mut n = 0;
|
|
walk(&slide.shapes, &mut |_, _| n += 1);
|
|
assert_eq!(n, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn text_body_helpers() {
|
|
let mut t = TextBody::from_text("Hello\nWorld");
|
|
assert_eq!(t.text(), "Hello\nWorld");
|
|
assert_eq!(t.char_len(), 11);
|
|
t.paragraphs[0].runs.push(Run::new(" there"));
|
|
t.paragraphs[0].normalize();
|
|
assert_eq!(t.paragraphs[0].runs.len(), 1);
|
|
assert_eq!(t.paragraphs[0].text(), "Hello there");
|
|
}
|
|
|
|
#[test]
|
|
fn builtin_themes_have_distinct_names() {
|
|
let t = theme::builtin_themes();
|
|
let mut names: Vec<_> = t.iter().map(|t| t.name.clone()).collect();
|
|
names.sort();
|
|
names.dedup();
|
|
assert_eq!(names.len(), t.len());
|
|
assert!(t.len() >= 8);
|
|
}
|
|
|
|
#[test]
|
|
fn four_by_three_master_scales() {
|
|
let p = defaults::blank_presentation(defaults::STANDARD, Theme::default(), true);
|
|
let m = &p.masters[0];
|
|
let title = m.shapes.iter().find(|s| s.ph_type() == Some(PhType::Title)).unwrap();
|
|
let x = title.xfrm.unwrap();
|
|
assert!(x.x + x.w <= 720.0 + 1e-6);
|
|
}
|
|
|
|
#[test]
|
|
fn hyperlinked_runs_take_the_theme_hyperlink_look() {
|
|
use crate::text::{Action, Hyperlink, Paragraph, RunProps};
|
|
let p = Presentation::default();
|
|
let s = &p.slides[0];
|
|
let ctx = Ctx::for_slide(&p, s).unwrap();
|
|
let title = &s.shapes[0];
|
|
let para = Paragraph::default();
|
|
let link = Some(Hyperlink { action: Action::Url { url: "https://example.org".into() }, tooltip: String::new(), highlight_click: false });
|
|
let hlink = Some(Fill::solid(ColorRef::scheme(SchemeSlot::Hlink)));
|
|
// The title's master style colours its text tx1; a link is hlink and underlined instead.
|
|
assert_eq!(resolve::run(&ctx, title, ¶, &RunProps::default()).fill, Some(Fill::solid(ColorRef::scheme(SchemeSlot::Tx1))));
|
|
let r = resolve::run(&ctx, title, ¶, &RunProps { link: link.clone(), ..Default::default() });
|
|
assert_eq!((r.fill, r.underline.as_deref()), (hlink.clone(), Some("sng")));
|
|
assert_eq!(ctx.color(&ColorRef::scheme(SchemeSlot::Hlink), None), p.masters[0].theme.colors.get(SchemeSlot::Hlink));
|
|
// A slide link looks the same.
|
|
let slide = Some(Hyperlink { action: Action::Slide { slide: s.id }, tooltip: String::new(), highlight_click: false });
|
|
assert_eq!(resolve::run(&ctx, title, ¶, &RunProps { link: slide, ..Default::default() }).fill, hlink);
|
|
// The run's own colour and underline win; u="none" removes the underline.
|
|
let red = Some(Fill::solid(ColorRef::rgb(deckcraft_color::Rgba::rgb(255, 0, 0))));
|
|
let own = RunProps { link: link.clone(), fill: red.clone(), underline: Some("none".into()), ..Default::default() };
|
|
let r = resolve::run(&ctx, title, ¶, &own);
|
|
assert_eq!((r.fill, r.underline.as_deref()), (red, Some("none")));
|
|
let dbl = resolve::run(&ctx, title, ¶, &RunProps { link, underline: Some("dbl".into()), ..Default::default() });
|
|
assert_eq!((dbl.fill, dbl.underline.as_deref()), (hlink, Some("dbl")));
|
|
}
|
|
|
|
#[test]
|
|
fn a_layout_colour_map_override_applies_to_its_slides() {
|
|
// A dark "section" layout maps bg1→dk1 and tx1→lt1; a slide on it that keeps the master
|
|
// mapping (`masterClrMapping`) follows the layout, as PowerPoint draws it.
|
|
let mut p = Presentation::default();
|
|
let dark: Vec<(String, String)> =
|
|
[("bg1", "dk1"), ("tx1", "lt1"), ("bg2", "dk2"), ("tx2", "lt2")].iter().map(|(a, b)| (a.to_string(), b.to_string())).collect();
|
|
let layout = p.slides[0].layout;
|
|
let m = std::sync::Arc::make_mut(&mut p.masters[0]);
|
|
let colors = m.theme.colors.clone();
|
|
m.layouts.iter_mut().find(|l| l.id == layout).unwrap().color_map = Some(dark.clone());
|
|
let s: Slide = (*p.slides[0]).clone();
|
|
let ctx = Ctx::for_slide(&p, &s).unwrap();
|
|
assert_eq!(ctx.color(&ColorRef::scheme(SchemeSlot::Bg1), None), colors.get(SchemeSlot::Dk1));
|
|
assert_eq!(ctx.color(&ColorRef::scheme(SchemeSlot::Tx2), None), colors.get(SchemeSlot::Lt2));
|
|
assert_eq!(ctx.scheme.get(SchemeSlot::Lt1), colors.get(SchemeSlot::Dk1));
|
|
// The slide's own override wins over the layout's.
|
|
let mut own = s.clone();
|
|
own.color_map = Some([("bg1", "lt1"), ("tx1", "dk1")].iter().map(|(a, b)| (a.to_string(), b.to_string())).collect());
|
|
let ctx = Ctx::for_slide(&p, &own).unwrap();
|
|
assert_eq!(ctx.color(&ColorRef::scheme(SchemeSlot::Bg1), None), colors.get(SchemeSlot::Lt1));
|
|
// The layout itself draws with its override; the master keeps its own map.
|
|
let m = &p.masters[0];
|
|
let l = m.layout(layout).unwrap();
|
|
assert_eq!(Ctx::for_layout(&p, m, l).color(&ColorRef::scheme(SchemeSlot::Bg1), None), colors.get(SchemeSlot::Dk1));
|
|
assert_eq!(Ctx::for_master(&p, m).color(&ColorRef::scheme(SchemeSlot::Bg1), None), colors.get(SchemeSlot::Lt1));
|
|
}
|
|
|
|
#[test]
|
|
fn sibling_lists_and_parents_reach_into_groups() {
|
|
let leaf = |id: u32| Shape { id: ShapeId(id), ..Default::default() };
|
|
let group = |id: u32, children: Vec<Shape>| Shape {
|
|
id: ShapeId(id),
|
|
kind: ShapeKind::Group { children, child: Default::default() },
|
|
..Default::default()
|
|
};
|
|
let mut tree = vec![leaf(1), group(2, vec![leaf(3), group(4, vec![leaf(5)])]), leaf(6)];
|
|
assert_eq!(parent_of(&tree, ShapeId(1)), Some(None));
|
|
assert_eq!(parent_of(&tree, ShapeId(3)), Some(Some(ShapeId(2))));
|
|
assert_eq!(parent_of(&tree, ShapeId(5)), Some(Some(ShapeId(4))));
|
|
assert_eq!(parent_of(&tree, ShapeId(9)), None);
|
|
let ids = |v: &Vec<Shape>| v.iter().map(|s| s.id.0).collect::<Vec<_>>();
|
|
assert_eq!(sibling_list_mut(&mut tree, ShapeId(6)).map(|v| ids(v)), Some(vec![1, 2, 6]));
|
|
assert_eq!(sibling_list_mut(&mut tree, ShapeId(4)).map(|v| ids(v)), Some(vec![3, 4]));
|
|
assert_eq!(sibling_list_mut(&mut tree, ShapeId(5)).map(|v| ids(v)), Some(vec![5]));
|
|
assert!(sibling_list_mut(&mut tree, ShapeId(9)).is_none());
|
|
}
|