tests.rs
raw
use super::*;
use crate::registry::Registry;
const MARKDOWN: &str = r##"
(widget markdown
(type "::guiduck_markdown::MarkdownView")
(prop source String)
(prop wrap bool :setter set_wrapping)
(event on-link :payload String)
(event on-render)
(token link-color Brush)
(token gutter 12)) ; not a type — checked below
"##;
/// The whole grammar in one pass: a widget's type path, its props (with and
/// without an explicit setter), its events (with and without a payload), and
/// its theme tokens.
#[test]
fn compiles_the_declared_interface() {
let source = r##"
(widget markdown
(type "::guiduck_markdown::MarkdownView")
(prop source String)
(prop wrap bool :setter set_wrapping)
(event on-link :payload String)
(event on-render)
(token link-color Brush)
(token gutter Number))
"##;
let widgets = compile_manifest(source).expect("valid manifest");
assert_eq!(widgets.len(), 1);
let markdown = &widgets[0];
assert_eq!(markdown.name, "markdown");
assert_eq!(markdown.type_path, "::guiduck_markdown::MarkdownView");
// Props, events, and queries share one ordered list; each carries its
// kind. The settable props come first, in file order.
let setters: Vec<_> = markdown
.props
.iter()
.filter_map(|p| match &p.kind {
PropDeclKind::Setter { method, ty } => Some((p.name.as_ref(), *ty, method.as_ref())),
_ => None,
})
.collect();
assert_eq!(
setters,
vec![
("source", PropTy::Scalar(TypeKind::String), "set_source"),
("wrap", PropTy::Scalar(TypeKind::Bool), "set_wrapping"),
],
"a prop's setter follows the convention unless `:setter` says otherwise"
);
let events: Vec<_> = markdown
.props
.iter()
.filter_map(|p| match &p.kind {
PropDeclKind::Event(EventRef::User { payload }) => Some((p.name.as_ref(), *payload)),
_ => None,
})
.collect();
assert_eq!(
events,
vec![("on-link", Some(TypeKind::String)), ("on-render", None)]
);
assert_eq!(
markdown.theme_tokens().collect::<Vec<_>>(),
vec![
("link-color", ThemeValueType::Brush),
("gutter", ThemeValueType::Number),
]
);
}
/// The dashed surface spelling maps to the Rust method convention.
#[test]
fn setter_convention_snake_cases_the_name() {
assert_eq!(setter_name("source"), "set_source");
assert_eq!(setter_name("link-color"), "set_link_color");
}
/// One manifest holds a crate's whole widget set.
#[test]
fn a_manifest_may_declare_several_widgets() {
let source = r##"
(widget markdown (type "::m::View"))
(widget code-block (type "::m::Code") (prop language String))
"##;
let widgets = compile_manifest(source).expect("valid manifest");
assert_eq!(
widgets.iter().map(|w| w.name.as_ref()).collect::<Vec<_>>(),
vec!["markdown", "code-block"]
);
}
fn errors(source: &str) -> Vec<String> {
compile_manifest(source)
.expect_err("invalid manifest")
.into_iter()
.map(|d| d.message)
.collect()
}
#[test]
fn rejects_a_widget_without_a_type() {
let messages = errors(r##"(widget markdown (prop source String))"##);
assert!(messages[0].contains("has no `(type …)`"), "{messages:?}");
}
#[test]
fn rejects_an_unknown_token_type() {
let messages = errors(r##"(widget markdown (type "::m::V") (token link-color Colour))"##);
assert!(
messages[0].contains("`Colour` is not a token type"),
"{messages:?}"
);
assert!(messages[0].contains("Brush"), "it lists the types");
}
#[test]
fn rejects_an_unknown_prop_type() {
let messages = errors(r##"(widget markdown (type "::m::V") (prop source Text))"##);
assert!(messages[0].contains("unknown type `Text`"), "{messages:?}");
}
/// Records are file-local to a `.gdc` and a list has no setter convention, so
/// neither crosses the manifest boundary.
#[test]
fn rejects_a_non_scalar_prop_type() {
let messages = errors(r##"(widget markdown (type "::m::V") (prop items (List i64)))"##);
assert!(messages[0].contains("expected a type"), "{messages:?}");
}
#[test]
fn rejects_a_duplicate_widget_in_one_manifest() {
let messages = errors(
r##"
(widget markdown (type "::m::A"))
(widget markdown (type "::m::B"))
"##,
);
assert!(
messages[0].contains("`markdown` is already declared in this manifest"),
"{messages:?}"
);
}
#[test]
fn rejects_duplicate_props_events_and_tokens() {
let messages = errors(
r##"
(widget markdown
(type "::m::V")
(prop source String)
(prop source i64)
(event on-link)
(event on-link :payload String)
(token link-color Brush)
(token link-color Number))
"##,
);
assert!(
messages[0].contains("`source` is already a prop"),
"{messages:?}"
);
assert!(
messages[1].contains("`on-link` is already an event"),
"{messages:?}"
);
assert!(
messages[2].contains("`link-color` is already a token"),
"{messages:?}"
);
}
/// A prop and an event are both `:name value` on a node, so they share one
/// namespace.
#[test]
fn a_prop_and_an_event_cannot_share_a_name() {
let messages =
errors(r##"(widget markdown (type "::m::V") (prop on-link String) (event on-link))"##);
assert!(
messages[0].contains("`on-link` is already a prop"),
"{messages:?}"
);
}
/// Redeclaring a universal would be silently dead: the lookup finds the
/// universal first.
#[test]
fn rejects_redeclaring_a_universal_property() {
for name in [
"width",
"on-click",
"class",
"tooltip",
"enabled",
"autofocus",
] {
let source = format!(r##"(widget markdown (type "::m::V") (prop {name} String))"##);
let messages = errors(&source);
assert!(
messages[0].contains("a property every widget already has"),
"`{name}`: {messages:?}"
);
}
}
/// The framework's structural forms: where a widget may sit, that a parent
/// builds it, and a menu accelerator — the capabilities that used to belong to
/// builtins alone, now declarable by any `.gdw`.
#[test]
fn declares_placement_writability_and_an_accelerator() {
let source = r##"
(widget fancy-item
(type "::app::FancyItem")
(parents menu context-menu)
(accel :setter set_accel_text)
(event on-select))
(widget fancy-panel
(type "::app::FancyPanel")
(internal))
"##;
let widgets = compile_manifest(source).expect("valid manifest");
let item = &widgets[0];
assert_eq!(
item.required_parents.as_ref(),
&[
std::borrow::Cow::Borrowed("menu"),
std::borrow::Cow::Borrowed("context-menu")
],
"`(parents …)` lands as the placement rule",
);
assert!(item.is_writable, "an item is written by a file");
assert!(
matches!(
item.lookup("accel").map(|d| &d.kind),
Some(PropDeclKind::Accel { method }) if method == "set_accel_text"
),
"`(accel …)` is a settable accelerator with the given method",
);
let panel = &widgets[1];
assert!(
!panel.is_writable,
"`(internal)` makes a widget one a parent builds, not one a file writes",
);
assert!(
panel.required_parents.is_empty(),
"no placement rule declared"
);
}
/// A widget's own props are its own: `corner-radius` is a container's, not
/// every widget's, so a manifest may take the name.
#[test]
fn a_builtins_own_property_name_is_not_reserved() {
let source = r##"(widget markdown (type "::m::V") (prop corner-radius f32))"##;
let widgets = compile_manifest(source).expect("valid manifest");
assert_eq!(widgets[0].props[0].name, "corner-radius");
}
#[test]
fn rejects_an_event_without_the_on_prefix() {
let messages = errors(r##"(widget markdown (type "::m::V") (event link))"##);
assert!(
messages[0].contains("event names start with `on-`"),
"{messages:?}"
);
}
/// A capitalized name in widget position is a component reference, so nothing
/// could ever write such a widget.
#[test]
fn rejects_a_capitalized_widget_name() {
let messages = errors(r##"(widget Markdown (type "::m::V"))"##);
assert!(
messages[0].contains("widget names are lowercase"),
"{messages:?}"
);
}
/// The reader terminates symbols at `:`, so a Rust path can never be one —
/// which is why the grammar takes a string, and says so when given a symbol.
#[test]
fn rejects_a_bare_type_path() {
let messages = errors(r##"(widget markdown (type MarkdownView))"##);
assert!(
messages[0].contains("`(type …)` takes one string"),
"{messages:?}"
);
// And the lexical half of the same fact: `::` cannot even be read.
let messages = errors(r##"(widget markdown (type ::m::V))"##);
assert!(
messages[0].contains("`:` must be followed by a name"),
"{messages:?}"
);
}
#[test]
fn rejects_an_unknown_form_inside_a_widget() {
let messages = errors(r##"(widget markdown (type "::m::V") (slot))"##);
assert!(messages[0].contains("expected `(type …)`"), "{messages:?}");
}
/// The registry, not the manifest, sees the whole vocabulary — so it is what
/// rejects a name a builtin or another manifest already has.
#[test]
fn the_registry_rejects_a_builtin_name() {
let widgets = compile_manifest(r##"(widget text (type "::m::V"))"##).expect("parses");
let error = Registry::default()
.insert(widgets.into_iter().next().unwrap())
.expect_err("`text` is a builtin");
assert!(
error
.message
.contains("`text` is already the name of a builtin widget"),
"{error:?}"
);
}
#[test]
fn the_registry_rejects_a_name_two_manifests_claim() {
let mut registry = Registry::default();
for source in [
r##"(widget markdown (type "::a::View"))"##,
r##"(widget markdown (type "::b::View"))"##,
]
.iter()
.enumerate()
{
let (i, source) = source;
let widget = compile_manifest(source)
.expect("parses")
.into_iter()
.next()
.unwrap();
match registry.insert(widget) {
Ok(()) => assert_eq!(i, 0, "the first claim wins the name"),
Err(error) => {
assert_eq!(i, 1);
assert!(
error
.message
.contains("`markdown` is already the name of another widget manifest"),
"{error:?}"
);
}
}
}
}
/// The registry answers about builtins and user widgets through one interface,
/// and the shared vocabulary reaches both.
#[test]
fn the_registry_unions_the_universal_vocabulary_into_a_user_widget() {
let mut registry = Registry::default();
for widget in compile_manifest(MARKDOWN_OK).expect("valid manifest") {
registry.insert(widget).expect("fresh names");
}
let markdown = registry.resolve("markdown").expect("declared");
assert_eq!(markdown.name, "markdown");
assert!(
matches!(
markdown.lookup("source").map(|d| &d.kind),
Some(PropDeclKind::Setter { .. })
),
"its own props resolve"
);
assert!(
matches!(
markdown.lookup("on-link").map(|d| &d.kind),
Some(PropDeclKind::Event(EventRef::User { .. }))
),
"its own events resolve"
);
assert!(
matches!(
markdown.lookup("width").map(|d| &d.kind),
Some(PropDeclKind::Layout(_))
),
"the universal layout properties reach it"
);
assert!(
matches!(
markdown.lookup("on-click").map(|d| &d.kind),
Some(PropDeclKind::Event(EventRef::Builtin(_)))
),
"the universal pointer events reach it"
);
assert!(
matches!(
markdown.lookup("autofocus").map(|d| &d.kind),
Some(PropDeclKind::Autofocus)
),
"`:autofocus` is universal, so a user widget takes it — and no-ops if it \
is not focusable",
);
assert!(markdown.lookup("nonesuch").is_none());
assert_eq!(
markdown.theme_value_type("link-color"),
Some(ThemeValueType::Brush)
);
assert_eq!(markdown.theme_value_type("background"), None);
let known = markdown.known_props();
for expected in ["source", "on-link", "width", "on-click", "class"] {
assert!(known.contains(&expected), "`{expected}` in {known:?}");
}
assert!(
registry.writable_names().contains("markdown"),
"the unknown-widget diagnostic names the application's own widgets"
);
}
const MARKDOWN_OK: &str = r##"
(widget markdown
(type "::guiduck_markdown::MarkdownView")
(prop source String)
(event on-link :payload String)
(token link-color Brush))
"##;
/// The manifest reader is the `.gdc` reader: a read error arrives as an
/// ordinary span-pointed diagnostic.
#[test]
fn reports_a_read_error() {
let messages = errors(r##"(widget markdown (type "::m::V")"##);
assert!(messages[0].contains("unclosed parenthesis"), "{messages:?}");
}
// The `MARKDOWN` sample above documents the shape; `gutter 12` in it is
// deliberately wrong, which this pins.
#[test]
fn a_token_type_is_a_symbol_not_a_value() {
let messages = errors(MARKDOWN);
assert!(
messages[0].contains("expected a token type"),
"{messages:?}"
);
}
/// The placement rules name their parents as strings now that there is no
/// closed kind enum. This recovers the typo-safety the enum gave: every name a
/// `required_parents` list mentions must resolve to a real, writable widget.
#[test]
fn every_required_parent_names_a_real_writable_widget() {
for widget in registry::builtins() {
for parent in widget.required_parents.iter() {
let declared = registry::builtin(parent).unwrap_or_else(|| {
panic!(
"`{}` requires a parent `{parent}`, which is not a widget",
widget.name
)
});
assert!(
declared.is_writable,
"`{}` requires a parent `{parent}`, which is not writable",
widget.name
);
}
}
}