Skip to main content

clawft_types/
canvas.rs

1//! Canvas protocol types for the Live Canvas system.
2//!
3//! Defines the protocol for agents to render UI elements on a canvas,
4//! receive interaction events, and manage canvas state. These types
5//! are serialized over WebSocket for real-time UI updates.
6
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Unique identifier for a canvas element.
11pub type ElementId = String;
12
13/// Unique identifier for a canvas instance.
14pub type CanvasId = String;
15
16// ── Default value helpers ─────────────────────────────────────────
17
18fn default_text_format() -> String {
19    "plain".into()
20}
21
22fn default_field_type() -> String {
23    "text".into()
24}
25
26fn default_chart_type() -> String {
27    "bar".into()
28}
29
30fn default_true() -> bool {
31    true
32}
33
34// ── Canvas elements ───────────────────────────────────────────────
35
36/// UI element types that agents can render on the canvas.
37///
38/// Each variant represents a different UI primitive. The `type` field
39/// is used as the serde tag for JSON serialization.
40#[non_exhaustive]
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
42#[serde(tag = "type", rename_all = "snake_case")]
43pub enum CanvasElement {
44    /// A text block with optional formatting.
45    Text {
46        content: String,
47        #[serde(default = "default_text_format")]
48        format: String,
49    },
50    /// A clickable button that triggers an action.
51    Button {
52        label: String,
53        action: String,
54        #[serde(default)]
55        disabled: bool,
56    },
57    /// A text input field.
58    Input {
59        label: String,
60        #[serde(default)]
61        placeholder: String,
62        #[serde(default)]
63        value: String,
64    },
65    /// An image element.
66    Image {
67        src: String,
68        #[serde(default)]
69        alt: String,
70    },
71    /// A code block with optional syntax highlighting.
72    Code {
73        code: String,
74        #[serde(default)]
75        language: String,
76    },
77    /// A data table with headers and rows.
78    Table {
79        headers: Vec<String>,
80        rows: Vec<Vec<String>>,
81    },
82    /// A form with multiple fields and a submit action.
83    Form {
84        fields: Vec<FormField>,
85        submit_action: String,
86    },
87    /// A chart element for data visualization.
88    Chart {
89        data: Vec<ChartDataPoint>,
90        #[serde(default = "default_chart_type")]
91        chart_type: String,
92        #[serde(default)]
93        title: Option<String>,
94        #[serde(default)]
95        colors: Option<Vec<String>>,
96    },
97    /// A code editor element with optional editing and line numbers.
98    CodeEditor {
99        code: String,
100        #[serde(default)]
101        language: String,
102        #[serde(default)]
103        editable: bool,
104        #[serde(default = "default_true")]
105        line_numbers: bool,
106    },
107    /// An advanced form with typed fields and validation.
108    FormAdvanced {
109        fields: Vec<AdvancedFormField>,
110        #[serde(default)]
111        submit_action: Option<String>,
112    },
113}
114
115/// A data point for chart elements.
116#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
117pub struct ChartDataPoint {
118    /// Label for this data point (x-axis or slice label).
119    pub label: String,
120    /// Numeric value for this data point.
121    pub value: f64,
122}
123
124/// A field within a [`CanvasElement::Form`].
125#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
126pub struct FormField {
127    /// Machine-readable field name (used as the key in form submission).
128    pub name: String,
129    /// Human-readable label shown to the user.
130    pub label: String,
131    /// The HTML input type (e.g., "text", "email", "number").
132    #[serde(default = "default_field_type")]
133    pub field_type: String,
134    /// Whether this field must be filled before submission.
135    #[serde(default)]
136    pub required: bool,
137    /// Placeholder text shown when the field is empty.
138    #[serde(default)]
139    pub placeholder: Option<String>,
140}
141
142/// A field within a [`CanvasElement::FormAdvanced`] with richer type info.
143#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
144pub struct AdvancedFormField {
145    /// Machine-readable field name.
146    pub name: String,
147    /// The field type: "text", "number", "select", "checkbox", "textarea".
148    #[serde(default = "default_field_type")]
149    pub field_type: String,
150    /// Human-readable label.
151    pub label: String,
152    /// Whether this field is required.
153    #[serde(default)]
154    pub required: bool,
155    /// Options for select fields.
156    #[serde(default)]
157    pub options: Option<Vec<String>>,
158    /// Minimum value for number fields.
159    #[serde(default)]
160    pub min: Option<f64>,
161    /// Maximum value for number fields.
162    #[serde(default)]
163    pub max: Option<f64>,
164    /// Placeholder text.
165    #[serde(default)]
166    pub placeholder: Option<String>,
167}
168
169// ── Canvas commands ───────────────────────────────────────────────
170
171/// Commands from agents to the canvas.
172///
173/// These are sent over WebSocket to create, update, remove, or batch
174/// operations on canvas elements.
175#[non_exhaustive]
176#[derive(Debug, Clone, Serialize, Deserialize)]
177#[serde(tag = "command", rename_all = "snake_case")]
178pub enum CanvasCommand {
179    /// Render a new element on the canvas.
180    Render {
181        id: ElementId,
182        element: CanvasElement,
183        #[serde(default)]
184        position: Option<u32>,
185    },
186    /// Update an existing element on the canvas.
187    Update {
188        id: ElementId,
189        element: CanvasElement,
190    },
191    /// Remove an element from the canvas.
192    Remove { id: ElementId },
193    /// Clear all elements from the canvas.
194    Reset,
195    /// Execute multiple commands atomically.
196    Batch { commands: Vec<CanvasCommand> },
197}
198
199// ── Canvas interactions ───────────────────────────────────────────
200
201/// Interaction events from the canvas back to agents.
202///
203/// These are generated by user interaction with canvas elements
204/// and sent back to the agent via WebSocket.
205#[non_exhaustive]
206#[derive(Debug, Clone, Serialize, Deserialize)]
207#[serde(tag = "interaction", rename_all = "snake_case")]
208pub enum CanvasInteraction {
209    /// User clicked a button element.
210    Click {
211        element_id: ElementId,
212        action: String,
213    },
214    /// User submitted an input field.
215    InputSubmit {
216        element_id: ElementId,
217        value: String,
218    },
219    /// User submitted a form.
220    FormSubmit {
221        element_id: ElementId,
222        values: HashMap<String, String>,
223    },
224    /// User submitted code from a code editor.
225    CodeSubmit {
226        element_id: ElementId,
227        code: String,
228        language: String,
229    },
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    // ── CanvasElement serialization ──────────────────────────────
237
238    #[test]
239    fn serialize_text_element() {
240        let elem = CanvasElement::Text {
241            content: "Hello, world!".into(),
242            format: "markdown".into(),
243        };
244        let json = serde_json::to_value(&elem).unwrap();
245        assert_eq!(json["type"], "text");
246        assert_eq!(json["content"], "Hello, world!");
247        assert_eq!(json["format"], "markdown");
248    }
249
250    #[test]
251    fn deserialize_text_element_with_default_format() {
252        let json = serde_json::json!({ "type": "text", "content": "hi" });
253        let elem: CanvasElement = serde_json::from_value(json).unwrap();
254        match elem {
255            CanvasElement::Text { content, format } => {
256                assert_eq!(content, "hi");
257                assert_eq!(format, "plain");
258            }
259            other => panic!("expected Text, got: {other:?}"),
260        }
261    }
262
263    #[test]
264    fn serialize_button_element() {
265        let elem = CanvasElement::Button {
266            label: "Click me".into(),
267            action: "do_thing".into(),
268            disabled: false,
269        };
270        let json = serde_json::to_value(&elem).unwrap();
271        assert_eq!(json["type"], "button");
272        assert_eq!(json["label"], "Click me");
273        assert_eq!(json["action"], "do_thing");
274        assert_eq!(json["disabled"], false);
275    }
276
277    #[test]
278    fn deserialize_button_element_default_disabled() {
279        let json = serde_json::json!({
280            "type": "button",
281            "label": "Go",
282            "action": "run"
283        });
284        let elem: CanvasElement = serde_json::from_value(json).unwrap();
285        match elem {
286            CanvasElement::Button { disabled, .. } => assert!(!disabled),
287            other => panic!("expected Button, got: {other:?}"),
288        }
289    }
290
291    #[test]
292    fn serialize_input_element() {
293        let elem = CanvasElement::Input {
294            label: "Name".into(),
295            placeholder: "Enter name".into(),
296            value: "".into(),
297        };
298        let json = serde_json::to_value(&elem).unwrap();
299        assert_eq!(json["type"], "input");
300        assert_eq!(json["label"], "Name");
301        assert_eq!(json["placeholder"], "Enter name");
302    }
303
304    #[test]
305    fn deserialize_input_element_defaults() {
306        let json = serde_json::json!({ "type": "input", "label": "Email" });
307        let elem: CanvasElement = serde_json::from_value(json).unwrap();
308        match elem {
309            CanvasElement::Input {
310                label,
311                placeholder,
312                value,
313            } => {
314                assert_eq!(label, "Email");
315                assert_eq!(placeholder, "");
316                assert_eq!(value, "");
317            }
318            other => panic!("expected Input, got: {other:?}"),
319        }
320    }
321
322    #[test]
323    fn serialize_image_element() {
324        let elem = CanvasElement::Image {
325            src: "https://example.com/img.png".into(),
326            alt: "Logo".into(),
327        };
328        let json = serde_json::to_value(&elem).unwrap();
329        assert_eq!(json["type"], "image");
330        assert_eq!(json["src"], "https://example.com/img.png");
331        assert_eq!(json["alt"], "Logo");
332    }
333
334    #[test]
335    fn deserialize_image_element_default_alt() {
336        let json = serde_json::json!({ "type": "image", "src": "a.png" });
337        let elem: CanvasElement = serde_json::from_value(json).unwrap();
338        match elem {
339            CanvasElement::Image { alt, .. } => assert_eq!(alt, ""),
340            other => panic!("expected Image, got: {other:?}"),
341        }
342    }
343
344    #[test]
345    fn serialize_code_element() {
346        let elem = CanvasElement::Code {
347            code: "fn main() {}".into(),
348            language: "rust".into(),
349        };
350        let json = serde_json::to_value(&elem).unwrap();
351        assert_eq!(json["type"], "code");
352        assert_eq!(json["code"], "fn main() {}");
353        assert_eq!(json["language"], "rust");
354    }
355
356    #[test]
357    fn deserialize_code_element_default_language() {
358        let json = serde_json::json!({ "type": "code", "code": "x = 1" });
359        let elem: CanvasElement = serde_json::from_value(json).unwrap();
360        match elem {
361            CanvasElement::Code { language, .. } => assert_eq!(language, ""),
362            other => panic!("expected Code, got: {other:?}"),
363        }
364    }
365
366    #[test]
367    fn serialize_table_element() {
368        let elem = CanvasElement::Table {
369            headers: vec!["Name".into(), "Age".into()],
370            rows: vec![vec!["Alice".into(), "30".into()]],
371        };
372        let json = serde_json::to_value(&elem).unwrap();
373        assert_eq!(json["type"], "table");
374        assert_eq!(json["headers"], serde_json::json!(["Name", "Age"]));
375        assert_eq!(json["rows"], serde_json::json!([["Alice", "30"]]));
376    }
377
378    #[test]
379    fn serialize_form_element() {
380        let elem = CanvasElement::Form {
381            fields: vec![FormField {
382                name: "username".into(),
383                label: "Username".into(),
384                field_type: "text".into(),
385                required: true,
386                placeholder: Some("Enter username".into()),
387            }],
388            submit_action: "create_user".into(),
389        };
390        let json = serde_json::to_value(&elem).unwrap();
391        assert_eq!(json["type"], "form");
392        assert_eq!(json["submit_action"], "create_user");
393        assert_eq!(json["fields"][0]["name"], "username");
394        assert_eq!(json["fields"][0]["required"], true);
395    }
396
397    // ── FormField serialization ─────────────────────────────────
398
399    #[test]
400    fn deserialize_form_field_defaults() {
401        let json = serde_json::json!({
402            "name": "email",
403            "label": "Email Address"
404        });
405        let field: FormField = serde_json::from_value(json).unwrap();
406        assert_eq!(field.name, "email");
407        assert_eq!(field.label, "Email Address");
408        assert_eq!(field.field_type, "text");
409        assert!(!field.required);
410        assert!(field.placeholder.is_none());
411    }
412
413    #[test]
414    fn form_field_roundtrip() {
415        let field = FormField {
416            name: "age".into(),
417            label: "Age".into(),
418            field_type: "number".into(),
419            required: false,
420            placeholder: Some("0".into()),
421        };
422        let json = serde_json::to_string(&field).unwrap();
423        let restored: FormField = serde_json::from_str(&json).unwrap();
424        assert_eq!(field, restored);
425    }
426
427    // ── Chart element tests ─────────────────────────────────────
428
429    #[test]
430    fn serialize_chart_element() {
431        let elem = CanvasElement::Chart {
432            data: vec![
433                ChartDataPoint {
434                    label: "Jan".into(),
435                    value: 100.0,
436                },
437                ChartDataPoint {
438                    label: "Feb".into(),
439                    value: 200.0,
440                },
441            ],
442            chart_type: "bar".into(),
443            title: Some("Monthly Revenue".into()),
444            colors: Some(vec!["#6366f1".into(), "#22c55e".into()]),
445        };
446        let json = serde_json::to_value(&elem).unwrap();
447        assert_eq!(json["type"], "chart");
448        assert_eq!(json["chart_type"], "bar");
449        assert_eq!(json["title"], "Monthly Revenue");
450        assert_eq!(json["data"].as_array().unwrap().len(), 2);
451        assert_eq!(json["data"][0]["label"], "Jan");
452        assert_eq!(json["data"][0]["value"], 100.0);
453    }
454
455    #[test]
456    fn deserialize_chart_element_defaults() {
457        let json = serde_json::json!({
458            "type": "chart",
459            "data": [{"label": "A", "value": 10}]
460        });
461        let elem: CanvasElement = serde_json::from_value(json).unwrap();
462        match elem {
463            CanvasElement::Chart {
464                data,
465                chart_type,
466                title,
467                colors,
468            } => {
469                assert_eq!(data.len(), 1);
470                assert_eq!(data[0].label, "A");
471                assert_eq!(data[0].value, 10.0);
472                assert_eq!(chart_type, "bar");
473                assert!(title.is_none());
474                assert!(colors.is_none());
475            }
476            other => panic!("expected Chart, got: {other:?}"),
477        }
478    }
479
480    #[test]
481    fn chart_data_point_roundtrip() {
482        let point = ChartDataPoint {
483            label: "March".into(),
484            value: 42.5,
485        };
486        let json = serde_json::to_string(&point).unwrap();
487        let restored: ChartDataPoint = serde_json::from_str(&json).unwrap();
488        assert_eq!(point, restored);
489    }
490
491    #[test]
492    fn chart_element_pie_type() {
493        let elem = CanvasElement::Chart {
494            data: vec![
495                ChartDataPoint {
496                    label: "Desktop".into(),
497                    value: 60.0,
498                },
499                ChartDataPoint {
500                    label: "Mobile".into(),
501                    value: 40.0,
502                },
503            ],
504            chart_type: "pie".into(),
505            title: Some("Device Share".into()),
506            colors: None,
507        };
508        let json = serde_json::to_value(&elem).unwrap();
509        assert_eq!(json["type"], "chart");
510        assert_eq!(json["chart_type"], "pie");
511        let roundtripped: CanvasElement = serde_json::from_value(json).unwrap();
512        assert_eq!(elem, roundtripped);
513    }
514
515    // ── CodeEditor element tests ────────────────────────────────
516
517    #[test]
518    fn serialize_code_editor_element() {
519        let elem = CanvasElement::CodeEditor {
520            code: "console.log('hello')".into(),
521            language: "javascript".into(),
522            editable: true,
523            line_numbers: true,
524        };
525        let json = serde_json::to_value(&elem).unwrap();
526        assert_eq!(json["type"], "code_editor");
527        assert_eq!(json["code"], "console.log('hello')");
528        assert_eq!(json["language"], "javascript");
529        assert_eq!(json["editable"], true);
530        assert_eq!(json["line_numbers"], true);
531    }
532
533    #[test]
534    fn deserialize_code_editor_element_defaults() {
535        let json = serde_json::json!({
536            "type": "code_editor",
537            "code": "x = 1"
538        });
539        let elem: CanvasElement = serde_json::from_value(json).unwrap();
540        match elem {
541            CanvasElement::CodeEditor {
542                code,
543                language,
544                editable,
545                line_numbers,
546            } => {
547                assert_eq!(code, "x = 1");
548                assert_eq!(language, "");
549                assert!(!editable);
550                assert!(line_numbers); // defaults to true
551            }
552            other => panic!("expected CodeEditor, got: {other:?}"),
553        }
554    }
555
556    #[test]
557    fn code_editor_roundtrip() {
558        let elem = CanvasElement::CodeEditor {
559            code: "fn main() {\n    println!(\"Hello\");\n}".into(),
560            language: "rust".into(),
561            editable: false,
562            line_numbers: false,
563        };
564        let json = serde_json::to_string(&elem).unwrap();
565        let restored: CanvasElement = serde_json::from_str(&json).unwrap();
566        assert_eq!(elem, restored);
567    }
568
569    // ── FormAdvanced element tests ──────────────────────────────
570
571    #[test]
572    fn serialize_form_advanced_element() {
573        let elem = CanvasElement::FormAdvanced {
574            fields: vec![
575                AdvancedFormField {
576                    name: "name".into(),
577                    field_type: "text".into(),
578                    label: "Full Name".into(),
579                    required: true,
580                    options: None,
581                    min: None,
582                    max: None,
583                    placeholder: Some("Enter your name".into()),
584                },
585                AdvancedFormField {
586                    name: "age".into(),
587                    field_type: "number".into(),
588                    label: "Age".into(),
589                    required: false,
590                    options: None,
591                    min: Some(0.0),
592                    max: Some(150.0),
593                    placeholder: None,
594                },
595                AdvancedFormField {
596                    name: "role".into(),
597                    field_type: "select".into(),
598                    label: "Role".into(),
599                    required: true,
600                    options: Some(vec!["Admin".into(), "User".into(), "Guest".into()]),
601                    min: None,
602                    max: None,
603                    placeholder: None,
604                },
605            ],
606            submit_action: Some("create_user".into()),
607        };
608        let json = serde_json::to_value(&elem).unwrap();
609        assert_eq!(json["type"], "form_advanced");
610        assert_eq!(json["submit_action"], "create_user");
611        assert_eq!(json["fields"].as_array().unwrap().len(), 3);
612        assert_eq!(json["fields"][0]["name"], "name");
613        assert_eq!(json["fields"][0]["required"], true);
614        assert_eq!(json["fields"][1]["min"], 0.0);
615        assert_eq!(json["fields"][2]["options"], serde_json::json!(["Admin", "User", "Guest"]));
616    }
617
618    #[test]
619    fn deserialize_advanced_form_field_defaults() {
620        let json = serde_json::json!({
621            "name": "notes",
622            "label": "Notes"
623        });
624        let field: AdvancedFormField = serde_json::from_value(json).unwrap();
625        assert_eq!(field.name, "notes");
626        assert_eq!(field.label, "Notes");
627        assert_eq!(field.field_type, "text");
628        assert!(!field.required);
629        assert!(field.options.is_none());
630        assert!(field.min.is_none());
631        assert!(field.max.is_none());
632        assert!(field.placeholder.is_none());
633    }
634
635    #[test]
636    fn advanced_form_field_roundtrip() {
637        let field = AdvancedFormField {
638            name: "priority".into(),
639            field_type: "select".into(),
640            label: "Priority".into(),
641            required: true,
642            options: Some(vec!["Low".into(), "Medium".into(), "High".into()]),
643            min: None,
644            max: None,
645            placeholder: Some("Select priority".into()),
646        };
647        let json = serde_json::to_string(&field).unwrap();
648        let restored: AdvancedFormField = serde_json::from_str(&json).unwrap();
649        assert_eq!(field, restored);
650    }
651
652    // ── CodeSubmit interaction tests ────────────────────────────
653
654    #[test]
655    fn serialize_code_submit_interaction() {
656        let interaction = CanvasInteraction::CodeSubmit {
657            element_id: "editor-1".into(),
658            code: "print('done')".into(),
659            language: "python".into(),
660        };
661        let json = serde_json::to_value(&interaction).unwrap();
662        assert_eq!(json["interaction"], "code_submit");
663        assert_eq!(json["element_id"], "editor-1");
664        assert_eq!(json["code"], "print('done')");
665        assert_eq!(json["language"], "python");
666    }
667
668    #[test]
669    fn deserialize_code_submit_interaction() {
670        let json = serde_json::json!({
671            "interaction": "code_submit",
672            "element_id": "ed-2",
673            "code": "x = 1",
674            "language": "python"
675        });
676        let interaction: CanvasInteraction = serde_json::from_value(json).unwrap();
677        match interaction {
678            CanvasInteraction::CodeSubmit {
679                element_id,
680                code,
681                language,
682            } => {
683                assert_eq!(element_id, "ed-2");
684                assert_eq!(code, "x = 1");
685                assert_eq!(language, "python");
686            }
687            other => panic!("expected CodeSubmit, got: {other:?}"),
688        }
689    }
690
691    // ── CanvasCommand serialization ─────────────────────────────
692
693    #[test]
694    fn serialize_render_command() {
695        let cmd = CanvasCommand::Render {
696            id: "el-1".into(),
697            element: CanvasElement::Text {
698                content: "Hello".into(),
699                format: "plain".into(),
700            },
701            position: Some(0),
702        };
703        let json = serde_json::to_value(&cmd).unwrap();
704        assert_eq!(json["command"], "render");
705        assert_eq!(json["id"], "el-1");
706        assert_eq!(json["element"]["type"], "text");
707        assert_eq!(json["position"], 0);
708    }
709
710    #[test]
711    fn deserialize_render_command_no_position() {
712        let json = serde_json::json!({
713            "command": "render",
714            "id": "el-2",
715            "element": { "type": "button", "label": "Go", "action": "run" }
716        });
717        let cmd: CanvasCommand = serde_json::from_value(json).unwrap();
718        match cmd {
719            CanvasCommand::Render { id, position, .. } => {
720                assert_eq!(id, "el-2");
721                assert!(position.is_none());
722            }
723            other => panic!("expected Render, got: {other:?}"),
724        }
725    }
726
727    #[test]
728    fn serialize_update_command() {
729        let cmd = CanvasCommand::Update {
730            id: "el-1".into(),
731            element: CanvasElement::Text {
732                content: "Updated".into(),
733                format: "markdown".into(),
734            },
735        };
736        let json = serde_json::to_value(&cmd).unwrap();
737        assert_eq!(json["command"], "update");
738        assert_eq!(json["id"], "el-1");
739    }
740
741    #[test]
742    fn serialize_remove_command() {
743        let cmd = CanvasCommand::Remove {
744            id: "el-3".into(),
745        };
746        let json = serde_json::to_value(&cmd).unwrap();
747        assert_eq!(json["command"], "remove");
748        assert_eq!(json["id"], "el-3");
749    }
750
751    #[test]
752    fn serialize_reset_command() {
753        let cmd = CanvasCommand::Reset;
754        let json = serde_json::to_value(&cmd).unwrap();
755        assert_eq!(json["command"], "reset");
756    }
757
758    #[test]
759    fn serialize_batch_command() {
760        let cmd = CanvasCommand::Batch {
761            commands: vec![
762                CanvasCommand::Reset,
763                CanvasCommand::Render {
764                    id: "el-1".into(),
765                    element: CanvasElement::Text {
766                        content: "Fresh".into(),
767                        format: "plain".into(),
768                    },
769                    position: None,
770                },
771            ],
772        };
773        let json = serde_json::to_value(&cmd).unwrap();
774        assert_eq!(json["command"], "batch");
775        assert_eq!(json["commands"].as_array().unwrap().len(), 2);
776    }
777
778    #[test]
779    fn deserialize_reset_command() {
780        let json = serde_json::json!({ "command": "reset" });
781        let cmd: CanvasCommand = serde_json::from_value(json).unwrap();
782        assert!(matches!(cmd, CanvasCommand::Reset));
783    }
784
785    // ── CanvasInteraction serialization ──────────────────────────
786
787    #[test]
788    fn serialize_click_interaction() {
789        let interaction = CanvasInteraction::Click {
790            element_id: "btn-1".into(),
791            action: "submit".into(),
792        };
793        let json = serde_json::to_value(&interaction).unwrap();
794        assert_eq!(json["interaction"], "click");
795        assert_eq!(json["element_id"], "btn-1");
796        assert_eq!(json["action"], "submit");
797    }
798
799    #[test]
800    fn serialize_input_submit_interaction() {
801        let interaction = CanvasInteraction::InputSubmit {
802            element_id: "input-1".into(),
803            value: "hello".into(),
804        };
805        let json = serde_json::to_value(&interaction).unwrap();
806        assert_eq!(json["interaction"], "input_submit");
807        assert_eq!(json["element_id"], "input-1");
808        assert_eq!(json["value"], "hello");
809    }
810
811    #[test]
812    fn serialize_form_submit_interaction() {
813        let mut values = HashMap::new();
814        values.insert("username".into(), "alice".into());
815        values.insert("email".into(), "alice@example.com".into());
816
817        let interaction = CanvasInteraction::FormSubmit {
818            element_id: "form-1".into(),
819            values,
820        };
821        let json = serde_json::to_value(&interaction).unwrap();
822        assert_eq!(json["interaction"], "form_submit");
823        assert_eq!(json["element_id"], "form-1");
824        assert_eq!(json["values"]["username"], "alice");
825        assert_eq!(json["values"]["email"], "alice@example.com");
826    }
827
828    #[test]
829    fn deserialize_click_interaction() {
830        let json = serde_json::json!({
831            "interaction": "click",
832            "element_id": "btn-x",
833            "action": "delete"
834        });
835        let interaction: CanvasInteraction = serde_json::from_value(json).unwrap();
836        match interaction {
837            CanvasInteraction::Click {
838                element_id,
839                action,
840            } => {
841                assert_eq!(element_id, "btn-x");
842                assert_eq!(action, "delete");
843            }
844            other => panic!("expected Click, got: {other:?}"),
845        }
846    }
847
848    #[test]
849    fn deserialize_form_submit_interaction() {
850        let json = serde_json::json!({
851            "interaction": "form_submit",
852            "element_id": "form-2",
853            "values": { "name": "Bob" }
854        });
855        let interaction: CanvasInteraction = serde_json::from_value(json).unwrap();
856        match interaction {
857            CanvasInteraction::FormSubmit {
858                element_id,
859                values,
860            } => {
861                assert_eq!(element_id, "form-2");
862                assert_eq!(values.get("name").unwrap(), "Bob");
863            }
864            other => panic!("expected FormSubmit, got: {other:?}"),
865        }
866    }
867
868    // ── Roundtrip tests ─────────────────────────────────────────
869
870    #[test]
871    fn canvas_element_roundtrip_all_variants() {
872        let elements = vec![
873            CanvasElement::Text {
874                content: "test".into(),
875                format: "markdown".into(),
876            },
877            CanvasElement::Button {
878                label: "OK".into(),
879                action: "confirm".into(),
880                disabled: true,
881            },
882            CanvasElement::Input {
883                label: "Query".into(),
884                placeholder: "Type here".into(),
885                value: "default".into(),
886            },
887            CanvasElement::Image {
888                src: "logo.png".into(),
889                alt: "Company Logo".into(),
890            },
891            CanvasElement::Code {
892                code: "print('hi')".into(),
893                language: "python".into(),
894            },
895            CanvasElement::Table {
896                headers: vec!["Col1".into()],
897                rows: vec![vec!["val".into()]],
898            },
899            CanvasElement::Form {
900                fields: vec![FormField {
901                    name: "f".into(),
902                    label: "Field".into(),
903                    field_type: "text".into(),
904                    required: false,
905                    placeholder: None,
906                }],
907                submit_action: "go".into(),
908            },
909            CanvasElement::Chart {
910                data: vec![ChartDataPoint {
911                    label: "Q1".into(),
912                    value: 42.0,
913                }],
914                chart_type: "line".into(),
915                title: Some("Quarterly".into()),
916                colors: None,
917            },
918            CanvasElement::CodeEditor {
919                code: "let x = 1;".into(),
920                language: "typescript".into(),
921                editable: true,
922                line_numbers: true,
923            },
924            CanvasElement::FormAdvanced {
925                fields: vec![AdvancedFormField {
926                    name: "email".into(),
927                    field_type: "text".into(),
928                    label: "Email".into(),
929                    required: true,
930                    options: None,
931                    min: None,
932                    max: None,
933                    placeholder: Some("you@example.com".into()),
934                }],
935                submit_action: Some("register".into()),
936            },
937        ];
938
939        for elem in &elements {
940            let json = serde_json::to_string(elem).unwrap();
941            let restored: CanvasElement = serde_json::from_str(&json).unwrap();
942            assert_eq!(*elem, restored);
943        }
944    }
945
946    // ── Render command with new element types ───────────────────
947
948    #[test]
949    fn render_chart_command() {
950        let cmd = CanvasCommand::Render {
951            id: "chart-1".into(),
952            element: CanvasElement::Chart {
953                data: vec![
954                    ChartDataPoint { label: "A".into(), value: 10.0 },
955                    ChartDataPoint { label: "B".into(), value: 20.0 },
956                ],
957                chart_type: "bar".into(),
958                title: None,
959                colors: None,
960            },
961            position: None,
962        };
963        let json = serde_json::to_value(&cmd).unwrap();
964        assert_eq!(json["command"], "render");
965        assert_eq!(json["element"]["type"], "chart");
966        assert_eq!(json["element"]["data"].as_array().unwrap().len(), 2);
967    }
968
969    #[test]
970    fn render_code_editor_command() {
971        let cmd = CanvasCommand::Render {
972            id: "editor-1".into(),
973            element: CanvasElement::CodeEditor {
974                code: "SELECT * FROM users;".into(),
975                language: "sql".into(),
976                editable: true,
977                line_numbers: true,
978            },
979            position: Some(0),
980        };
981        let json = serde_json::to_value(&cmd).unwrap();
982        assert_eq!(json["command"], "render");
983        assert_eq!(json["element"]["type"], "code_editor");
984        assert_eq!(json["element"]["editable"], true);
985    }
986}