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
            );
        }
    }
}