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![ ("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!["markdown", "code-block"] ); } fn errors(source: &str) -> Vec { 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 ); } } }