//! IR snapshots over the component corpus, and rendered-diagnostic //! snapshots for representative errors. `cargo insta review` to inspect //! changes. use guiduck_component_core::{ImportResolver, compile, compile_with, diagnostics}; fn compile_or_render_errors(source: &str, name: &str) -> String { match compile(source) { Ok(ir) => format!("{ir:#?}"), Err(diags) => diagnostics::render(&diags, source, name), } } /// An in-memory resolver over `(name, source)` pairs. struct MapResolver(&'static [(&'static str, &'static str)]); impl ImportResolver for MapResolver { fn resolve(&mut self, name: &str) -> Result<(String, String), String> { self.0 .iter() .find(|(n, _)| *n == name) .map(|(n, source)| (format!("{n}.gdc"), source.to_string())) .ok_or_else(|| "no such file".to_owned()) } } fn compile_nested_or_render_errors( source: &str, name: &str, imports: &'static [(&'static str, &'static str)], ) -> String { match compile_with(source, &mut MapResolver(imports)) { Ok(compiled) => format!("{compiled:#?}"), Err(diags) => diagnostics::render(&diags, source, name), } } #[test] fn counter_ir() { let source = include_str!("../../guiduck/components/Counter.gdc"); insta::assert_snapshot!(compile_or_render_errors(source, "Counter.gdc")); } #[test] fn kitchen_sink_ir() { let source = include_str!("corpus/kitchen-sink.gdc"); insta::assert_snapshot!(compile_or_render_errors(source, "kitchen-sink.gdc")); } #[test] fn diagnostics_unknown_property() { let source = r##" (component Broken (handler go) (container :colour "#fff" :on-click go (text :text "hi"))) "##; insta::assert_snapshot!(compile_or_render_errors(source, "broken.gdc")); } #[test] fn diagnostics_unresolved_names_and_bad_handler() { let source = r##" (component Broken (state count i32 :init 0) (container :on-click missing (text :text "{typo} and {count}"))) "##; insta::assert_snapshot!(compile_or_render_errors(source, "broken.gdc")); } #[test] fn diagnostics_reactive_layout_prop() { let source = r##" (component Broken (state wide bool :init false) (container :width (if wide 200 100) (text :text "hi"))) "##; insta::assert_snapshot!(compile_or_render_errors(source, "broken.gdc")); } #[test] fn diagnostics_bad_color_and_duplicate() { let source = r##" (component Broken (state x i32 :init 0) (state x i32 :init 1) (container :background "#notacolor" (text :text "hi"))) "##; insta::assert_snapshot!(compile_or_render_errors(source, "broken.gdc")); } #[test] fn text_input_ir() { let source = r##" (component EchoForm (state current String :init "") (output changed String) (handler edited String) (container :direction column :padding 16 :gap 12 :align-items flex-start (text-input :font-size 14 :font-family "DejaVu Sans" :width 260 :autofocus true :on-change edited) (text :font-size 14 :text "You typed: {current}"))) "##; insta::assert_snapshot!(compile_or_render_errors(source, "EchoForm.gdc")); } #[test] fn diagnostics_on_change_payload_mismatches() { // Three ways to get the payload contract wrong: a payload-less handler // on :on-change, a payload handler on :on-click, and :on-change on a // widget that has no such event. let source = r##" (component Broken (handler plain) (handler with-payload String) (container :direction column :on-click with-payload (text-input :on-change plain) (text :text "hi" :on-change plain))) "##; insta::assert_snapshot!(compile_or_render_errors(source, "broken.gdc")); } #[test] fn diagnostics_autofocus_misuse() { let source = r##" (component Broken (state on bool :init true) (container :direction column (text-input :autofocus on) (text-input :autofocus true) (text-input :autofocus true))) "##; insta::assert_snapshot!(compile_or_render_errors(source, "broken.gdc")); } #[test] fn diagnostics_enabled_misuse() { let source = r##" (component Broken (state busy bool :init false) (container :direction column (text :text "a" :enabled "yes") (text :text "b" :enabled 3) (text :text "c" :enabled missing))) "##; insta::assert_snapshot!(compile_or_render_errors(source, "broken.gdc")); } #[test] fn dynamic_structure_ir() { let source = r##" (component Dynamic (state todos (List String) :init (list "a" "b")) (state busy bool :init false) (container :direction column (if (not busy) (text :text "ready") (text :text "working")) (for todo todos :key todo :index i (text :text "{i}: {todo}")))) "##; insta::assert_snapshot!(compile_or_render_errors(source, "Dynamic.gdc")); } #[test] fn diagnostics_dynamic_misuse() { let source = r##" (component Broken (prop title String) (state todos (List String) :init (list)) (state count i32 :init 0) (container :direction column (if "nope" (text :text "a")) (for title todos (text :text "shadowed")) (for x count (text :text "not a list")) (for y missing (text :text "unknown")) (for z todos (text-input :autofocus true)))) "##; insta::assert_snapshot!(compile_or_render_errors(source, "broken.gdc")); } #[test] fn diagnostics_list_payloads_rejected() { let source = r##" (component Broken (output picked (List String)) (handler chose (List i32)) (container (text :text "x"))) "##; insta::assert_snapshot!(compile_or_render_errors(source, "broken.gdc")); } #[test] fn diagnostics_invocation_misuse() { let source = r##" (component Broken (state count i32 :init 0) (handler two i64 i64) (handler plain) (handler edited String) (container :direction column (container :on-click (two count)) (container :on-click (missing count)) (container :on-click (count count)) (container :on-click two) (text-input :on-change (edited count)) (text :text (plain)))) "##; insta::assert_snapshot!(compile_or_render_errors(source, "broken.gdc")); } #[test] fn records_ir() { let source = include_str!("../../guiduck/components/TodoList.gdc"); insta::assert_snapshot!(compile_or_render_errors(source, "TodoList.gdc")); } #[test] fn diagnostics_record_misuse() { let source = r##" (component Broken (record Todo :id i64 :label String) (record Todo :id i64) (record Nested :inner Todo) (record Listy :items (List i64)) (prop wrong Todo) (output bad Todo) (state single Todo :init (Todo :id 1 :label "x")) (state todos (List Todo) :init (list (Todo :id 1) (Todo :id 2 :label "b" :extra 3) (Todo :id "nope" :label "c"))) (state strs (List String) :init (list "a")) (container :direction column (for s strs :key s.id (text :text "{s.id}")) (for todo todos :key todo.missing (text :text "{todo.label.deep}")))) "##; insta::assert_snapshot!(compile_or_render_errors(source, "broken.gdc")); } #[test] fn diagnostics_payload_misuse() { let source = r##" (component Broken (state payload i32 :init 0) (handler save String) (handler plain) (container :direction column (container :on-click (plain)) (container :on-click (save payload)) (text-input :on-change (save (+ payload "!"))) (text :text "{payload}"))) "##; insta::assert_snapshot!(compile_or_render_errors(source, "broken.gdc")); } #[test] fn counter_panel_nested_ir() { let source = include_str!("../../guiduck/components/CounterPanel.gdc"); insta::assert_snapshot!(compile_nested_or_render_errors( source, "CounterPanel.gdc", &[( "CounterButton", include_str!("../../guiduck/components/CounterButton.gdc"), )], )); } #[test] fn diagnostics_component_instance_misuse() { // Missing required prop, unknown prop, unknown output, an // interior-layout property, children under a slotless instance, and a // duplicate prop — all checked against the child's compiled interface. // (`:class` and the item-layout properties are *legal* on instances.) let source = r##" (component Broken (handler tally i32) (container :direction column (Child :on-stepped tally) (Child :name "a" :name "b" :size 3 :on-missed tally :class "x" :grow 1 :direction row (text :text "no slots")))) "##; insta::assert_snapshot!(compile_nested_or_render_errors( source, "broken.gdc", &[( "Child", r#"(component Child (prop name String) (output stepped i32) (container (text :text "{name}")))"#, )], )); } #[test] fn diagnostics_component_wiring_mismatches() { // Output-wire payload contract violations and a handler colliding with // the generated logic-factory method name. let source = r##" (component Broken (handler plain) (handler wrong-type String) (handler the-child) (container :direction column (TheChild :name "a" :on-stepped plain) (TheChild :name "b" :on-stepped wrong-type))) "##; insta::assert_snapshot!(compile_nested_or_render_errors( source, "broken.gdc", &[( "TheChild", r#"(component TheChild (prop name String) (output stepped i32) (container (text :text "{name}")))"#, )], )); } #[test] fn diagnostics_component_cycle_and_unresolvable() { let source = r##" (component Broken (container (Alpha) (Nowhere))) "##; insta::assert_snapshot!(compile_nested_or_render_errors( source, "broken.gdc", &[ ("Alpha", r#"(component Alpha (container (Beta)))"#), ("Beta", r#"(component Beta (container (Alpha)))"#), ], )); } #[test] fn diagnostics_autofocus_across_composition() { // The single-autofocus rule spans the composed tree: this file's own // `:autofocus` plus each instantiated component that carries one. let source = r##" (component Broken (container :direction column (text-input :autofocus true) (Focusy) (Focusy))) "##; insta::assert_snapshot!(compile_nested_or_render_errors( source, "broken.gdc", &[( "Focusy", r#"(component Focusy (container (text-input :autofocus true)))"#, )], )); } #[test] fn diagnostics_reserved_prop_names() { let source = r##" (component Broken (prop class String) (prop on-fire bool :default false) (container (text :text "{class}"))) "##; insta::assert_snapshot!(compile_or_render_errors(source, "broken.gdc")); } #[test] fn widgets_ir() { let source = include_str!("../../guiduck/components/Settings.gdc"); insta::assert_snapshot!(compile_or_render_errors(source, "Settings.gdc")); } #[test] fn diagnostics_widget_misuse() { // A button has no `:checked`; a checkbox's `:on-toggle` must be wired to // a `(handler … bool)`; `:checked` takes a bool, not a string. let source = r##" (component Broken (state flag bool :init false) (handler wrong String) (container :direction column (button :label "go" :checked true) (checkbox :checked "yes" :on-toggle (wrong flag)) (checkbox :on-toggle unknown-handler))) "##; insta::assert_snapshot!(compile_or_render_errors(source, "broken.gdc")); } #[test] fn diagnostics_event_misuse() { // Where `event` may and may not be read, and what it offers. let source = r##" (component Broken (state n i32 :init 0) (handler pick i32) (handler edited String) (container :direction column ; not a field it has (container :on-click (pick event.button)) ; not a value in its own right (container :on-click (pick event)) ; not a pointer wire (text-input :text "x" :on-change (pick event.click-count)) ; not readable outside a wire's arguments (text :text "{event.x}"))) "##; insta::assert_snapshot!(compile_or_render_errors(source, "broken.gdc")); } #[test] fn diagnostics_graphic_misuse() { // Where a graphic may and may not go. (What an asset *path* may say is a // property of the form itself, unit-tested in `asset::tests`; those are // read-time errors, so they surface one at a time like any syntax error.) let source = r##" (component Broken (state name String :init "logo") (container :direction column ; a graphic property needs a graphic, not a bare string… (image :source "icons/logo.png") ; …and not an expression: the bytes are found at build time (image :source name) ; a graphic is not a string (text :text (asset "icons/logo.png")) ; nor a boolean (text :text "x" :enabled (asset "icons/logo.png")) ; nor something to compute with (text :text "x" :enabled (not (path (move 0 0)))))) "##; insta::assert_snapshot!(compile_or_render_errors(source, "broken.gdc")); } #[test] fn diagnostics_slot_misuse() { // Two slots, a slot with props and children, and a slot at the root. let source = r##" (component Broken (container :direction column (slot) (slot :grow 1 (text :text "no")))) "##; insta::assert_snapshot!(compile_or_render_errors(source, "broken.gdc")); } #[test] fn diagnostics_slot_at_root() { let source = r##" (component Broken (slot)) "##; insta::assert_snapshot!(compile_or_render_errors(source, "broken.gdc")); } #[test] fn diagnostics_key_misuse() { // A duplicate key on the same type, a reserved `#` prefix, and a // non-literal value. let source = r##" (component Broken (state which String :init "a") (container :direction column (Child :key "left") (Child :key "left") (Child :key "#0") (Child :key which))) "##; insta::assert_snapshot!(compile_nested_or_render_errors( source, "broken.gdc", &[("Child", r#"(component Child (container (text :text "x")))"#)], )); } /// The widget manifest a `.gdc` in these tests is compiled against. const WIDGET_MANIFEST: &str = r##" (widget markdown (type "::guiduck_markdown::MarkdownView") (prop source String) (prop wrap bool :setter set_wrapping) (prop gutter f32) (event on-link :payload String) (token link-color Brush)) "##; fn widget_registry() -> guiduck_component_core::registry::Registry { let mut registry = guiduck_component_core::registry::Registry::default(); for widget in guiduck_component_core::manifest::compile_manifest(WIDGET_MANIFEST) .expect("the manifest compiles") { registry.insert(widget).expect("fresh names"); } registry } fn compile_with_widgets_or_render_errors(source: &str, name: &str) -> String { struct NoImports; impl ImportResolver for NoImports { fn resolve(&mut self, _name: &str) -> Result<(String, String), String> { Err("no components here".into()) } } match guiduck_component_core::compile_with_widgets(source, &mut NoImports, &widget_registry()) { Ok(compiled) => format!("{:#?}", compiled.root), Err(diags) => diagnostics::render(&diags, source, name), } } /// A manifest-declared widget written in a `.gdc`: its own props static and /// bound, its own event wired, and the universal vocabulary — layout, /// `:class`, a pointer event — reaching it exactly as it reaches a builtin. #[test] fn user_widget_ir() { let source = r##" (component Page (state body String :init "# Hi") (handler navigate String) (handler clicked) (container :direction column :padding 8 (markdown :source body :wrap true :gutter 12 :width 400 :class "doc" :tooltip "The rendered page" :on-link (navigate payload) :on-click clicked))) "##; insta::assert_snapshot!(compile_with_widgets_or_render_errors(source, "Page.gdc")); } /// Every way to get a manifest-declared widget wrong from the `.gdc` side. #[test] fn diagnostics_user_widget_misuse() { let source = r##" (component Broken (state count i32 :init 0) (handler plain) (container :direction column (markdown :source 42) (markdown :source "ok" :nonesuch 1) (markdown :source "ok" :on-link plain) (markdown :source count) (markdown :source "ok" :gutter (asset "x.png")) (markdow :source "ok"))) "##; insta::assert_snapshot!(compile_with_widgets_or_render_errors(source, "broken.gdc")); } #[test] fn diagnostics_focus_and_key_misuse() { // `:focused`, `:on-key`, and what `event` offers on each kind of wire. let source = r##" (component Broken (state n i32 :init 0) (state flag bool :init false) (handler pick i32) (handler note String) (container :direction column ; `:focused` is a boolean expression, not a string (text-input :text "x" :focused "yes") ; a key wire has no position (container :on-key (pick event.x)) ; a pointer wire has no keystroke (container :on-click (note event.key)) ; a semantic wire has neither (checkbox :checked flag :on-toggle (note event.key)))) "##; insta::assert_snapshot!(compile_or_render_errors(source, "broken.gdc")); } #[test] fn diagnostics_mount_hook_collisions() { // The framework calls one logic method the file does not declare, so a // handler — or a child component whose generated factory method takes the // same name — is named rather than silently shadowing the hook. let source = r##" (component Broken (handler mounted) (container :direction column (Mounted))) "##; insta::assert_snapshot!(compile_nested_or_render_errors( source, "broken.gdc", &[("Mounted", "(component Mounted (text :text \"hi\"))")], )); }