snapshots_test.rs
raw
//! 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\"))")],
));
}