//! A parameter whose value is lines of text must be declared so, or the editor offers it in //! a one-line field — and a browser strips the newlines from a one-line field's value. //! //! That is how a statechart edited in the inspector came back as a single line: a chart that //! no longer parses, so the node quietly passed its state through and nothing said why. //! `Widget::multiline_parameters` is the declaration; these tests hold the library to it. use flowrs_core::graph::value::Value; use flowrs_core::graph::widget::{ExecContext, WidgetRegistry, edits_as_lines}; use std::collections::HashMap; #[test] fn every_parameter_whose_default_spans_lines_is_declared() { let reg = WidgetRegistry::with_builtins(); let mut spanning = 0; let mut undeclared = Vec::new(); for w in reg.all() { for p in w.parameters() { if p.default.contains(['\n', '\r']) { spanning += 1; if !w.multiline_parameters().contains(&p.name.as_str()) { undeclared.push(format!("{}.{}", w.id(), p.name)); } } } } // A guard over an empty set passes and proves nothing. The statechart's chart and the // inline table's CSV both default to several lines. assert!(spanning >= 2, "found only {spanning} multi-line defaults"); assert!( undeclared.is_empty(), "these default to several lines but would be edited in a one-line field, which \ drops the line breaks: {undeclared:?}" ); } #[test] fn every_declared_name_is_a_parameter_of_its_widget() { // The declaration is a list of names beside `parameters()`, so a rename on one side // leaves the other pointing at nothing — and the parameter quietly back in a one-line // field. let reg = WidgetRegistry::with_builtins(); let mut declared = 0; let mut dangling = Vec::new(); for w in reg.all() { let names: Vec = w.parameters().into_iter().map(|p| p.name).collect(); for name in w.multiline_parameters() { declared += 1; if !names.iter().any(|n| n == name) { dangling.push(format!("{}.{name}", w.id())); } } } assert!(declared >= 5, "found only {declared} declarations"); assert!( dangling.is_empty(), "declared multi-line but not a parameter: {dangling:?}" ); } #[test] fn line_oriented_parameters_are_edited_as_lines_before_they_hold_anything() { // Each of these is lines by its format, not by its current value: a statechart, CSV, // a bundle written one question per line, and prompts, which are prose. let reg = WidgetRegistry::with_builtins(); for (id, name) in [ ("flow.statechart", "machine"), ("data.table", "csv"), ("ai.hybrid", "bundle"), ("ai.hybrid", "prompt"), ("ai.complete", "prompt"), ] { let w = reg .get(id) .unwrap_or_else(|| panic!("{id} is not registered")); assert!(edits_as_lines(Some(w), name, ""), "{id}.{name}"); } // A one-line parameter stays one line. let print = reg.get("io.print").expect("io.print"); assert!(!edits_as_lines(Some(print), "label", "total")); let hybrid = reg.get("ai.hybrid").expect("ai.hybrid"); assert!(!edits_as_lines(Some(hybrid), "options", "yes, no")); } #[test] fn a_value_holding_a_line_break_is_edited_as_lines_whatever_was_declared() { // Whoever wrote the break (a file, an agent, an importer) meant it, and a one-line // field would delete it on the next edit. Code nodes from a Rust import are not // registry widgets at all, and their `src` is routinely several lines. let reg = WidgetRegistry::with_builtins(); let print = reg.get("io.print").expect("io.print"); assert!(edits_as_lines(Some(print), "label", "first\nsecond")); assert!(edits_as_lines(None, "src", "fn f() {\n 1\n}")); assert!(edits_as_lines(None, "src", "old\rmac")); assert!(!edits_as_lines(None, "src", "x + 1")); } /// What is at stake, shown on the widget that motivated this: the same chart, flattened the /// way a one-line field flattens it, does not run. #[test] fn a_statechart_runs_its_added_lines_and_not_once_flattened() { let reg = WidgetRegistry::with_builtins(); let chart = reg.get("flow.statechart").expect("flow.statechart"); let state_of = |machine: &str| { let props = HashMap::from([("machine".to_string(), machine.to_string())]); let out = chart.execute(&ExecContext::for_widget(chart, &props), HashMap::new()); match out.get("state") { Some(Value::String(s)) => s.clone(), other => panic!("expected a state, got {other:?}"), } }; let default = chart .parameters() .into_iter() .find(|p| p.name == "machine") .expect("machine") .default; assert_eq!(state_of(&default), "off"); // A state and an eventless transition into it, added as two more lines. let edited = format!("{default}state: lit\nalways: off -> lit\n"); assert_eq!(state_of(&edited), "lit"); // What an hands back: every line break gone. let flattened = edited.replace('\n', ""); assert_ne!(state_of(&flattened), "lit"); }