use std::collections::BTreeMap;
mod common;
use common::*;
fn doc_with_styles(tokens: Vec<Token>, styles: Vec<Style>, pages: Vec<Page>) -> Document {
Document {
version: 1,
colorspace: None,
doc_id: None,
mirror_margins: None,
facing_pages: None,
spread_gutter: None,
page_progression: None,
page_parity_start: None,
margin_inner: None,
margin_outer: None,
margin_top: None,
margin_bottom: None,
project: None,
assets: AssetBlock::default(),
libraries: Vec::new(),
actions: Vec::new(),
tokens: TokenBlock {
format: "zenith-token-v1".to_owned(),
tokens,
},
styles: StyleBlock {
styles,
source_span: None,
},
components: Vec::new(),
masters: Vec::new(),
sections: Vec::new(),
provenance: Vec::new(),
variants: Vec::new(),
recipes: Vec::new(),
diagnostic_policy: zenith_core::DiagnosticPolicy::default(),
brand_contract: BrandContract::default(),
body: DocumentBody {
id: "doc.main".to_owned(),
title: None,
block_styles: Vec::new(),
pages,
},
}
}
fn style_with_props(id: &str, props: Vec<(&str, PropertyValue)>) -> Style {
Style {
id: id.to_owned(),
properties: props.into_iter().map(|(k, v)| (k.to_owned(), v)).collect(),
unknown_props: BTreeMap::new(),
source_span: None,
}
}
#[test]
fn node_unknown_style_reference() {
let rect = match minimal_rect("rect.one", None) {
Node::Rect(mut r) => {
r.style = Some("style.missing".to_owned());
Node::Rect(r)
}
other => other,
};
let doc = doc_with_styles(
vec![],
vec![], vec![minimal_page("page.one", vec![rect])],
);
let report = validate(&doc);
assert!(
has_code(&report, "style.unknown_reference"),
"expected style.unknown_reference; codes: {:?}",
codes(&report)
);
assert!(report.has_errors());
}
#[test]
fn clean_code_node_no_errors() {
let doc = doc_with(
vec![color_token("color.fg")],
vec![minimal_page(
"page.one",
vec![minimal_code("code.one", Some(token_ref("color.fg")))],
)],
);
let report = validate(&doc);
assert!(
report.diagnostics.is_empty(),
"expected no diagnostics, got: {:?}",
codes(&report)
);
assert!(!report.has_errors());
}
#[test]
fn code_node_unknown_style_reference() {
let code = match minimal_code("code.one", None) {
Node::Code(mut c) => {
c.style = Some("style.missing".to_owned());
Node::Code(c)
}
other => other,
};
let doc = doc_with_styles(
vec![],
vec![], vec![minimal_page("page.one", vec![code])],
);
let report = validate(&doc);
assert!(
has_code(&report, "style.unknown_reference"),
"expected style.unknown_reference; codes: {:?}",
codes(&report)
);
assert!(report.has_errors());
}
#[test]
fn code_node_unknown_property_warns() {
let code = match minimal_code("code.one", None) {
Node::Code(mut c) => {
c.unknown_props.insert(
"future-prop".to_owned(),
zenith_core::UnknownProperty {
value: zenith_core::UnknownValue::String("x".to_owned()),
ty: None,
},
);
Node::Code(c)
}
other => other,
};
let doc = doc_with(vec![], vec![minimal_page("page.one", vec![code])]);
let report = validate(&doc);
assert!(
has_code(&report, "node.unknown_property"),
"expected node.unknown_property; codes: {:?}",
codes(&report)
);
assert!(!report.has_errors());
}
#[test]
fn style_prop_unknown_token() {
let style = style_with_props(
"style.s",
vec![("fill", PropertyValue::TokenRef("color.missing".to_owned()))],
);
let doc = doc_with_styles(
vec![], vec![style],
vec![minimal_page("page.one", vec![])],
);
let report = validate(&doc);
assert!(
has_code(&report, "token.unknown_reference"),
"expected token.unknown_reference; codes: {:?}",
codes(&report)
);
assert!(report.has_errors());
}
#[test]
fn style_raw_literal_fill() {
let style = style_with_props(
"style.s",
vec![("fill", PropertyValue::Literal("#ff0000".to_owned()))],
);
let doc = doc_with_styles(vec![], vec![style], vec![minimal_page("page.one", vec![])]);
let report = validate(&doc);
assert!(
has_code(&report, "token.raw_visual_literal"),
"expected token.raw_visual_literal; codes: {:?}",
codes(&report)
);
assert!(report.has_errors());
}
#[test]
fn style_padding_raw_literal_rejected() {
let style = style_with_props(
"style.flow",
vec![("padding", PropertyValue::Dimension(px(16.0)))],
);
let doc = doc_with_styles(vec![], vec![style], vec![minimal_page("page.one", vec![])]);
let report = validate(&doc);
assert!(
has_code(&report, "token.raw_visual_literal"),
"a raw-literal padding must flag token.raw_visual_literal; codes: {:?}",
codes(&report)
);
assert!(report.has_errors());
}
#[test]
fn style_gap_raw_literal_rejected() {
let style = style_with_props(
"style.flow",
vec![("gap", PropertyValue::Dimension(px(8.0)))],
);
let doc = doc_with_styles(vec![], vec![style], vec![minimal_page("page.one", vec![])]);
let report = validate(&doc);
assert!(
has_code(&report, "token.raw_visual_literal"),
"a raw-literal gap must flag token.raw_visual_literal; codes: {:?}",
codes(&report)
);
assert!(report.has_errors());
}
#[test]
fn style_unknown_property_warns() {
let style = Style {
id: "style.s".to_owned(),
properties: BTreeMap::new(),
unknown_props: {
let mut m = BTreeMap::new();
m.insert(
"bogus-prop".to_owned(),
UnknownStyleProp {
raw: "whatever".to_owned(),
},
);
m
},
source_span: None,
};
let doc = doc_with_styles(vec![], vec![style], vec![minimal_page("page.one", vec![])]);
let report = validate(&doc);
assert!(
has_code(&report, "style.unknown_property"),
"expected style.unknown_property warning; codes: {:?}",
codes(&report)
);
let diag = report
.diagnostics
.iter()
.find(|d| d.code == "style.unknown_property")
.expect("must exist");
assert_eq!(diag.severity, Severity::Warning);
assert!(
!report.has_errors(),
"unknown prop must only warn, not error"
);
}
#[test]
fn token_used_only_by_style_not_unused() {
let style = style_with_props(
"style.s",
vec![("fill", PropertyValue::TokenRef("color.used".to_owned()))],
);
let doc = doc_with_styles(
vec![color_token("color.used")],
vec![style],
vec![minimal_page("page.one", vec![])],
);
let report = validate(&doc);
let unused: Vec<_> = report
.diagnostics
.iter()
.filter(|d| d.code == "token.unused")
.collect();
assert!(
unused.is_empty(),
"token referenced by style must not be flagged token.unused; codes: {:?}",
codes(&report)
);
}
fn minimal_code(id: &str, fill: Option<PropertyValue>) -> Node {
Node::Code(CodeNode {
id: id.to_owned(),
name: None,
role: None,
x: Some(pxv(0.0)),
y: Some(pxv(0.0)),
w: Some(pxv(200.0)),
h: Some(pxv(80.0)),
overflow: None,
language: None,
line_numbers: None,
tab_width: None,
style: None,
fill,
font_family: None,
font_size: None,
font_weight: None,
syntax_theme: None,
opacity: None,
visible: None,
locked: None,
selectable: None,
rotate: None,
anchor: None,
anchor_zone: None,
anchor_sibling: None,
anchor_edge: None,
anchor_gap: None,
anchor_parent: None,
content: String::new(),
source_span: None,
unknown_props: BTreeMap::new(),
})
}
fn doc_with_libraries(libraries: Vec<LibraryDef>, pages: Vec<Page>) -> Document {
let mut doc = doc_with(vec![], pages);
doc.libraries = libraries;
doc
}
fn minimal_library(id: &str) -> LibraryDef {
LibraryDef {
id: id.to_owned(),
version: None,
hash: None,
source_span: None,
unknown_props: BTreeMap::new(),
}
}
#[test]
fn clean_libraries_block_no_diagnostics() {
let lib = LibraryDef {
id: "@acme/brand-kit".to_owned(),
version: Some("1.4.0".to_owned()),
hash: Some("sha256-abc".to_owned()),
source_span: None,
unknown_props: BTreeMap::new(),
};
let doc = doc_with_libraries(vec![lib], vec![minimal_page("p1", vec![])]);
let report = validate(&doc);
assert!(
report.diagnostics.is_empty(),
"a well-formed libraries block must produce no diagnostics; got: {:?}",
codes(&report)
);
assert!(!report.has_errors());
}
#[test]
fn library_duplicate_id_is_error() {
let a = minimal_library("@acme/brand-kit");
let b = minimal_library("@acme/brand-kit"); let doc = doc_with_libraries(vec![a, b], vec![minimal_page("p1", vec![])]);
let report = validate(&doc);
assert!(
has_code(&report, "id.duplicate"),
"two libraries sharing an id must trigger id.duplicate; got {:?}",
codes(&report)
);
assert!(report.has_errors());
}
#[test]
fn library_unknown_property_produces_warning() {
let mut unknown_props = BTreeMap::new();
unknown_props.insert(
"registry".to_owned(),
zenith_core::UnknownProperty {
value: zenith_core::UnknownValue::String("x".to_owned()),
ty: Some("token".to_owned()),
},
);
let lib = LibraryDef {
id: "@acme/brand-kit".to_owned(),
version: None,
hash: None,
source_span: None,
unknown_props,
};
let doc = doc_with_libraries(vec![lib], vec![minimal_page("p1", vec![])]);
let report = validate(&doc);
assert!(
has_code(&report, "library.unknown_property"),
"an unknown prop on a library must warn; got {:?}",
codes(&report)
);
let diag = report
.diagnostics
.iter()
.find(|d| d.code == "library.unknown_property")
.expect("should exist");
assert_eq!(diag.severity, Severity::Warning);
assert!(!report.has_errors());
}
fn doc_with_provenance(
provenance: Vec<ProvenanceDef>,
libraries: Vec<LibraryDef>,
children: Vec<Node>,
) -> Document {
let mut doc = doc_with(vec![], vec![minimal_page("p1", children)]);
doc.libraries = libraries;
doc.provenance = provenance;
doc
}
fn minimal_provenance(id: &str, node: &str, library: &str) -> ProvenanceDef {
ProvenanceDef {
id: id.to_owned(),
node: node.to_owned(),
library: library.to_owned(),
item: None,
linked: None,
source_span: None,
unknown_props: BTreeMap::new(),
}
}
#[test]
fn clean_provenance_record_no_diagnostics() {
let prov = ProvenanceDef {
id: "prov.btn".to_owned(),
node: "btn".to_owned(),
library: "@acme/brand-kit".to_owned(),
item: Some("button".to_owned()),
linked: Some(true),
source_span: None,
unknown_props: BTreeMap::new(),
};
let doc = doc_with_provenance(
vec![prov],
vec![minimal_library("@acme/brand-kit")],
vec![minimal_rect("btn", None)],
);
let report = validate(&doc);
assert!(
report.diagnostics.is_empty(),
"a fully-resolved provenance record must produce no diagnostics; got: {:?}",
codes(&report)
);
assert!(!report.has_errors());
}
#[test]
fn provenance_unknown_node_is_error() {
let prov = minimal_provenance("prov.ghost", "ghost", "@acme/brand-kit");
let doc = doc_with_provenance(
vec![prov],
vec![minimal_library("@acme/brand-kit")],
vec![minimal_rect("btn", None)],
);
let report = validate(&doc);
assert!(
has_code(&report, "provenance.unknown_node"),
"a provenance record referencing a non-existent node must error; got {:?}",
codes(&report)
);
assert!(report.has_errors());
}
#[test]
fn provenance_node_may_be_a_declared_token() {
let prov = minimal_provenance("prov.noir", "noir", "@zenith/filters");
let mut doc = doc_with_provenance(
vec![prov],
vec![minimal_library("@zenith/filters")],
vec![minimal_rect("btn", None)],
);
doc.tokens.tokens.push(color_token_hex("noir", "#000000"));
let report = validate(&doc);
assert!(
!has_code(&report, "provenance.unknown_node"),
"a provenance record targeting a declared token must not error; got {:?}",
codes(&report)
);
}
#[test]
fn provenance_unknown_library_is_error() {
let prov = minimal_provenance("prov.btn", "btn", "@nope/x");
let doc = doc_with_provenance(
vec![prov],
vec![minimal_library("@acme/brand-kit")],
vec![minimal_rect("btn", None)],
);
let report = validate(&doc);
assert!(
has_code(&report, "provenance.unknown_library"),
"a provenance record referencing an undeclared library must error; got {:?}",
codes(&report)
);
assert!(report.has_errors());
}
#[test]
fn provenance_duplicate_id_is_error() {
let a = minimal_provenance("prov.dup", "btn", "@acme/brand-kit");
let b = minimal_provenance("prov.dup", "btn", "@acme/brand-kit"); let doc = doc_with_provenance(
vec![a, b],
vec![minimal_library("@acme/brand-kit")],
vec![minimal_rect("btn", None)],
);
let report = validate(&doc);
assert!(
has_code(&report, "id.duplicate"),
"two provenance records sharing an id must trigger id.duplicate; got {:?}",
codes(&report)
);
assert!(report.has_errors());
}
#[test]
fn provenance_unknown_property_produces_warning() {
let mut unknown_props = BTreeMap::new();
unknown_props.insert(
"registry".to_owned(),
zenith_core::UnknownProperty {
value: zenith_core::UnknownValue::String("x".to_owned()),
ty: Some("token".to_owned()),
},
);
let prov = ProvenanceDef {
id: "prov.btn".to_owned(),
node: "btn".to_owned(),
library: "@acme/brand-kit".to_owned(),
item: None,
linked: None,
source_span: None,
unknown_props,
};
let doc = doc_with_provenance(
vec![prov],
vec![minimal_library("@acme/brand-kit")],
vec![minimal_rect("btn", None)],
);
let report = validate(&doc);
assert!(
has_code(&report, "provenance.unknown_property"),
"an unknown prop on an origin must warn; got {:?}",
codes(&report)
);
let diag = report
.diagnostics
.iter()
.find(|d| d.code == "provenance.unknown_property")
.expect("should exist");
assert_eq!(diag.severity, Severity::Warning);
assert!(!report.has_errors());
}
#[test]
fn provenance_node_may_be_a_declared_action() {
let prov = minimal_provenance("prov.brand", "apply-brand-kit", "@acme/brand-kit");
let mut doc = doc_with_provenance(vec![prov], vec![minimal_library("@acme/brand-kit")], vec![]);
doc.actions.push(ActionDef {
id: "apply-brand-kit".to_owned(),
label: Some("Apply Brand Kit".to_owned()),
version: None,
tx_json: "{}".to_owned(),
source_span: None,
unknown_props: BTreeMap::new(),
});
let report = validate(&doc);
assert!(
!has_code(&report, "provenance.unknown_node"),
"a provenance record targeting a declared action must not fire unknown_node; got {:?}",
codes(&report)
);
assert!(!report.has_errors());
}
#[test]
fn provenance_node_nonexistent_id_still_errors() {
let prov = minimal_provenance("prov.ghost2", "does-not-exist", "@acme/brand-kit");
let doc = doc_with_provenance(
vec![prov],
vec![minimal_library("@acme/brand-kit")],
vec![minimal_rect("btn", None)],
);
let report = validate(&doc);
assert!(
has_code(&report, "provenance.unknown_node"),
"a provenance record with a non-existent node/token/action id must error; got {:?}",
codes(&report)
);
assert!(report.has_errors());
}