use crossterm::event::{ Event, KeyCode as K, KeyEvent, KeyModifiers as M, MouseButton as MB, MouseEvent, MouseEventKind as MK, }; use ratatui::{backend::TestBackend, layout::Rect, Terminal}; use std::{ fs, path::PathBuf, sync::atomic::{AtomicUsize, Ordering}, }; use tb_frontend::forms::{self, ObjectClass as C}; use tb_ide::{ app::{App, DialogKind, Field, Hit, Mode, WindowKind}, commands::Command, designer::{self, DesignId}, documents::{Destination, Project}, }; use tb_ui::{forms::PropertyValue as V, frm}; struct Temp(PathBuf); impl Temp { fn new() -> Self { static N: AtomicUsize = AtomicUsize::new(0); let p = std::env::temp_dir().join(format!( "tb-designer-{}-{}", std::process::id(), N.fetch_add(1, Ordering::Relaxed) )); fs::create_dir_all(&p).unwrap(); Self(p.canonicalize().unwrap()) } fn app(&self) -> App { let mut a = App::new(&self.0, self.0.join("options"), (100, 30)).unwrap(); a.options.syntax_checking = false; a.execute(Command::NewForm); assert_eq!(a.mode, Mode::Designer); a } } impl Drop for Temp { fn drop(&mut self) { let _ = fs::remove_dir_all(&self.0); } } fn key(a: &mut App, k: K, m: M) { a.handle(Event::Key(KeyEvent::new(k, m))); } fn plain(a: &mut App, k: K) { key(a, k, M::NONE); } fn root(a: &mut App) -> DesignId { a.design_objects().unwrap()[0].0 } fn node(a: &mut App, id: DesignId) -> frm::FormNode { a.design_objects() .unwrap() .into_iter() .find(|(i, _)| *i == id) .unwrap() .1 } fn number(a: &mut App, id: DesignId, p: &str) -> i32 { match designer::value(&node(a, id), p).unwrap() { V::Integer(n) => n, _ => panic!(), } } fn draw(a: &mut App) -> String { let mut t = Terminal::new(TestBackend::new(100, 30)).unwrap(); t.draw(|f| a.render(f)).unwrap(); t.backend() .buffer() .content .iter() .map(|c| c.symbol()) .collect() } fn mouse(a: &mut App, kind: MK, x: u16, y: u16, m: M) { a.handle(Event::Mouse(MouseEvent { kind, column: x, row: y, modifiers: m, })); } fn click(a: &mut App, x: u16, y: u16) { mouse(a, MK::Down(MB::Left), x, y, M::NONE); mouse(a, MK::Up(MB::Left), x, y, M::NONE); } fn property(a: &mut App, p: &str, v: &str) { a.design_property(p, v).unwrap(); } #[test] fn all_metadata_tools_persist_and_preview_matches_runtime_without_events() { let t = Temp::new(); let mut a = t.app(); let root = root(&mut a); property(&mut a, "WIDTH", "70"); property(&mut a, "HEIGHT", "24"); for (i, (_, class, style)) in designer::tools().iter().enumerate() { a.design_place( *class, *style, root, Rect::new(1 + (i % 8) as u16 * 8, 1 + (i / 8) as u16 * 6, 6, 3), ) .unwrap(); } let timer = a .design_objects() .unwrap() .into_iter() .find(|(_, n)| n.class == C::Timer) .unwrap() .0; a.design_select(timer, false).unwrap(); property(&mut a, "INTERVAL", "1"); let doc = a.design_document().unwrap(); a.project.replace_text(doc,0..0,"SUB Form_Load\nPRINT \"NO PREVIEW EXECUTION\"\nEND SUB\nSUB Timer1_Timer\nPRINT \"NO TIMER\"\nEND SUB\n").unwrap(); let form = a.design_form().unwrap().clone(); let (mut model, screen) = designer::preview(&form, (100, 30)).unwrap(); assert!(model.events.is_empty()); assert!(model.next_deadline().is_none()); model.timers(10_000); assert!(model.next_event().is_none()); assert!(a.session.vm.is_none()); let mut runtime = tb_ui::forms::FormsModel::new(form.catalog().objects, 100, 30); form.apply(&mut runtime).unwrap(); runtime.show(0, false).unwrap(); let mut expected = tb_runtime::screen::TextScreen::with_size(100, 30); runtime.render(&mut expected); for y in 1..=30 { for x in 1..=100 { assert_eq!(screen.cell(y, x), expected.cell(y, x)); } } let path = t.0.join("designed.frm"); a.project .save_file(doc, Some(&Destination::new(&path))) .unwrap(); let reopened = Project::open(&path, vec![]).unwrap(); let tb_vm::project_io::Content::Form(f) = reopened.document(reopened.members()[0]).unwrap().content() else { panic!() }; assert_equivalent(&f.root, &form.root); assert_eq!(f.root.children.len(), designer::tools().len()); tb_vm::project_io::load_program(&path).unwrap(); let listed = a .menus() .into_iter() .find(|m| m.title == "Tools") .unwrap() .items; assert_eq!(listed.len(), designer::tools().len()); } #[test] fn container_multiselect_keyboard_drag_resize_and_undo() { let t = Temp::new(); let mut a = t.app(); let root = root(&mut a); let frame = a .design_place(C::Frame, 0, root, Rect::new(3, 2, 28, 10)) .unwrap(); let b1 = a .design_place(C::CommandButton, 0, frame, Rect::new(2, 2, 8, 2)) .unwrap(); let b2 = a .design_place(C::TextBox, 0, frame, Rect::new(12, 2, 8, 2)) .unwrap(); a.design_select(b1, false).unwrap(); a.design_select(b2, true).unwrap(); key(&mut a, K::Right, M::CONTROL); assert_eq!(number(&mut a, b1, "LEFT"), 7); assert_eq!(number(&mut a, b2, "LEFT"), 17); assert_eq!(number(&mut a, frame, "LEFT"), 3); a.execute(Command::Undo); assert_eq!(number(&mut a, b1, "LEFT"), 2); assert_eq!(number(&mut a, b2, "LEFT"), 12); a.design_select(b1, false).unwrap(); key(&mut a, K::Right, M::SHIFT); assert_eq!(number(&mut a, b1, "WIDTH"), 9); a.execute(Command::Undo); draw(&mut a); let r = a .designer .objects .iter() .find(|(id, _)| *id == b1) .unwrap() .1; assert_eq!(r.x, a.designer.viewport.x + 3 + 2); assert_eq!(r.y, a.designer.viewport.y + 2 + 2); mouse(&mut a, MK::Down(MB::Left), r.x + 2, r.y, M::NONE); mouse(&mut a, MK::Drag(MB::Left), r.x + 3, r.y + 1, M::NONE); mouse(&mut a, MK::Up(MB::Left), r.x + 3, r.y + 1, M::NONE); assert_eq!(number(&mut a, b1, "LEFT"), 3); assert_eq!(number(&mut a, b1, "TOP"), 3); draw(&mut a); let r = a .designer .objects .iter() .find(|(id, _)| *id == b1) .unwrap() .1; mouse( &mut a, MK::Down(MB::Left), r.right() - 1, r.bottom() - 1, M::NONE, ); mouse(&mut a, MK::Up(MB::Left), r.right(), r.bottom(), M::NONE); assert_eq!(number(&mut a, b1, "WIDTH"), 9); assert_eq!(number(&mut a, b1, "HEIGHT"), 3); let before = a.design_form().unwrap().clone(); assert!(a.design_geometry(-100, 0, false).is_err()); assert_eq!(*a.design_form().unwrap(), before); a.design_select(b1, false).unwrap(); plain(&mut a, K::Tab); assert_eq!( a.designer.selections[&a.design_document().unwrap()], vec![b2] ); plain(&mut a, K::BackTab); assert_eq!( a.designer.selections[&a.design_document().unwrap()], vec![b1] ); draw(&mut a); let r = a .designer .objects .iter() .find(|(id, _)| *id == b2) .unwrap() .1; mouse(&mut a, MK::Down(MB::Left), r.x + 1, r.y, M::CONTROL); mouse(&mut a, MK::Up(MB::Left), r.x + 1, r.y, M::CONTROL); assert_eq!( a.designer.selections[&a.design_document().unwrap()].len(), 2 ); } #[test] fn property_bar_types_readonly_and_no_status_line() { let t = Temp::new(); let mut a = t.app(); let root = root(&mut a); let b = a .design_place(C::CommandButton, 0, root, Rect::new(2, 3, 8, 2)) .unwrap(); let before = a.design_form().unwrap().clone(); for (p, v) in [ ("HEIGHT", "0"), ("WIDTH", "x"), ("BACKCOLOR", "16"), ("ENABLED", "yes"), ("PARENT", "anything"), ] { let e = a.design_property(p, v).unwrap_err(); assert!(format!("{e:#}").contains(p)); assert_eq!(*a.design_form().unwrap(), before); } a.designer.property = forms::properties(C::CommandButton) .iter() .position(|p| p.name == "CAPTION") .unwrap(); plain(&mut a, K::F(2)); assert!(a.value_focus); key(&mut a, K::Char('a'), M::CONTROL); for c in "Hallo".chars() { plain(&mut a, K::Char(c)); } plain(&mut a, K::Enter); assert_eq!( designer::value(&node(&mut a, b), "CAPTION"), Some(V::String("Hallo".into())) ); let screen = draw(&mut a); assert!(screen.contains("2,3")); assert!(screen.contains("8×2")); assert!(!screen.contains("Shift+F1=Help")); plain(&mut a, K::F(10)); assert!(!a.properties && a.menu.is_some()); plain(&mut a, K::F(10)); assert!(a.properties && a.menu.is_none()); let spin = a .design_place(C::Spin, 0, root, Rect::new(15, 3, 1, 2)) .unwrap(); assert!(a.design_property("WIDTH", "4").is_err()); property(&mut a, "STYLE", "1"); assert_eq!(number(&mut a, spin, "WIDTH"), 2); assert_eq!(number(&mut a, spin, "HEIGHT"), 1); } #[test] fn clipboard_children_arrays_and_delete_preserve_code() { let t = Temp::new(); let mut a = t.app(); let root = root(&mut a); let frame = a .design_place(C::Frame, 0, root, Rect::new(1, 1, 20, 10)) .unwrap(); let b = a .design_place(C::CommandButton, 0, frame, Rect::new(1, 1, 8, 2)) .unwrap(); property(&mut a, "INDEX", "0"); a.design_event(b, "CLICK").unwrap(); a.design_event(b, "CLICK").unwrap(); key(&mut a, K::F(12), M::SHIFT); let code = a.design_form().unwrap().code.clone(); a.design_select(frame, false).unwrap(); a.execute(Command::Copy); a.design_select(root, false).unwrap(); a.execute(Command::Paste); assert!(a.dialog.is_none(), "{:?}", a.dialog); let f = a.design_form().unwrap(); assert_eq!(f.root.children.len(), 2); let copied = &f.root.children[1]; assert_eq!(copied.children.len(), 1); assert_ne!(copied.children[0].name, f.root.children[0].children[0].name); assert_eq!(designer::node_key(&copied.children[0]).1, Some(0)); a.execute(Command::Undo); assert_eq!(a.design_form().unwrap().root.children.len(), 1); a.design_select(frame, false).unwrap(); a.execute(Command::Cut); assert_eq!(a.design_form().unwrap().code, code); assert!(a.message.contains("ungebunden")); plain(&mut a, K::Esc); a.execute(Command::Undo); assert_eq!(a.design_form().unwrap().root.children.len(), 1); assert_eq!(node(&mut a, b).class, C::CommandButton); } #[test] fn bound_rename_across_documents_is_atomic_and_undo_restores_all() { let t = Temp::new(); let mut a = t.app(); let root = root(&mut a); let b = a .design_place(C::CommandButton, 0, root, Rect::new(2, 2, 8, 2)) .unwrap(); let name = node(&mut a, b).name; let form = a.design_form().unwrap().root.name.clone(); let doc = a.design_document().unwrap(); let code=format!("SUB {name}_Click\n{name}.Caption = \"{name} unchanged\"\n' {name}.Caption unchanged\nEND SUB\n"); a.project.replace_text(doc, 0..0, &code).unwrap(); let other = a.project.new_module("Other").unwrap(); let external = format!("PRINT {form}!{name}.Caption\n"); a.project.replace_text(other, 0..0, &external).unwrap(); a.design_rename("GoButton").unwrap(); assert_eq!(node(&mut a, b).name, "GoButton"); assert!(a .project .document(doc) .unwrap() .code() .contains("SUB GoButton_CLICK")); assert!(a .project .document(doc) .unwrap() .code() .contains("GoButton.Caption")); assert!(a .project .document(doc) .unwrap() .code() .contains(&format!("\"{name} unchanged\""))); assert!(a .project .document(other) .unwrap() .code() .contains("!GoButton.Caption")); a.project.undo(other).unwrap(); assert_eq!(a.project.document(other).unwrap().code(), external); assert_eq!(a.project.document(doc).unwrap().code(), code); assert_eq!(node(&mut a, b).name, name); let conflict = a .design_place(C::TextBox, 0, root, Rect::new(14, 2, 8, 2)) .unwrap(); let conflict_name = node(&mut a, conflict).name; a.design_select(b, false).unwrap(); let before = a.design_form().unwrap().clone(); assert!(a.design_rename(&conflict_name).is_err()); assert_eq!(*a.design_form().unwrap(), before); a.project.replace_text(other, 0..0, "IF \n").unwrap(); assert!(a.design_rename("Unsafe").is_err()); assert_eq!(*a.design_form().unwrap(), before); } #[test] fn event_array_signature_twice_and_shared_form_identity() { let t = Temp::new(); let mut a = t.app(); let root = root(&mut a); let b = a .design_place(C::TextBox, 0, root, Rect::new(2, 2, 8, 2)) .unwrap(); property(&mut a, "INDEX", "0"); let doc = a.design_document().unwrap(); let structure = a.design_form().unwrap().root.clone(); a.design_event(b, "DRAGOVER").unwrap(); assert_eq!(a.mode, Mode::Environment); let code = a.project.document(doc).unwrap().code().to_owned(); assert!(code.contains( "Index AS INTEGER, SOURCE AS CONTROL, X AS SINGLE, Y AS SINGLE, STATE AS INTEGER" )); a.compile_current().unwrap(); let view = if let WindowKind::Code(v) = a.active_window().unwrap().kind { v } else { panic!() }; let cursor = a.project.view(view).unwrap().cursor; key(&mut a, K::F(12), M::SHIFT); assert_eq!(a.mode, Mode::Designer); assert_eq!(a.active_document(), Some(doc)); assert_eq!(a.design_form().unwrap().root, structure); assert_eq!(a.designer.selections[&doc], vec![b]); a.design_event(b, "DRAGOVER").unwrap(); assert_eq!(a.project.document(doc).unwrap().code(), code); assert_eq!(a.project.view(view).unwrap().cursor, cursor); key(&mut a, K::F(12), M::SHIFT); plain(&mut a, K::F(12)); assert!(matches!( a.dialog.as_ref().unwrap().kind, DialogKind::DesignObject(_) )); plain(&mut a, K::Enter); assert!(matches!( a.dialog.as_ref().unwrap().kind, DialogKind::DesignEvent(_) )); plain(&mut a, K::Enter); assert_eq!(a.mode, Mode::Environment); assert!(a.dialog.is_none()); } #[test] fn menu_hierarchy_arrays_shortcuts_and_palette_use_events() { let t = Temp::new(); let mut a = t.app(); let root = root(&mut a); let title = a.design_place(C::Menu, 0, root, Rect::default()).unwrap(); property(&mut a, "CAPTION", "&File"); let sub = a.design_place(C::Menu, 0, title, Rect::default()).unwrap(); property(&mut a, "CAPTION", "&Open"); property(&mut a, "INDEX", "0"); property(&mut a, "SHORTCUT", "F3"); let second = a.design_place(C::Menu, 0, title, Rect::default()).unwrap(); property(&mut a, "CAPTION", "-"); property(&mut a, "SEPARATOR", "true"); assert!(a.design_property("CHECKED", "true").is_err()); a.design_select(sub, false).unwrap(); a.design_menu_move("down").unwrap(); a.design_menu_move("up").unwrap(); assert!(a.design_property("SHORTCUT", "F11").is_err()); a.execute(Command::MenuDesign); draw(&mut a); assert_eq!(a.active_window().unwrap().kind, WindowKind::MenuDesign); plain(&mut a, K::Enter); assert!(matches!( a.dialog.as_ref().unwrap().kind, DialogKind::DesignMenu(_) )); a.dialog.as_mut().unwrap().fields[2] = Field::text("Tag", "kept"); plain(&mut a, K::Enter); assert!(a.dialog.is_none(), "{:?}", a.dialog); a.design_select(root, false).unwrap(); a.execute(Command::Palette); plain(&mut a, K::Enter); a.dialog.as_mut().unwrap().fields[0] = Field::choice("ForeColor", (0..16).map(|n| n.to_string()).collect(), 4); a.dialog.as_mut().unwrap().fields[1] = Field::choice("BackColor", (0..16).map(|n| n.to_string()).collect(), 1); a.dialog.as_mut().unwrap().fields[2] = Field::toggle("Form", true); plain(&mut a, K::Enter); assert_eq!(number(&mut a, root, "FORECOLOR"), 4); assert_eq!(number(&mut a, root, "BACKCOLOR"), 1); let path = t.0.join("menu.frm"); let doc = a.design_document().unwrap(); a.project .save_file(doc, Some(&Destination::new(&path))) .unwrap(); let reopened = frm::read_text("menu", &fs::read_to_string(&path).unwrap()).unwrap(); assert_equivalent(&reopened.root, &a.design_form().unwrap().root); tb_vm::project_io::load_program(&path).unwrap(); assert_eq!(node(&mut a, second).name, "Menu3"); } #[test] fn toolbox_double_click_tool_drag_grid_and_window_form_selection() { let t = Temp::new(); let mut a = t.app(); let root = root(&mut a); a.execute(Command::Toolbox); draw(&mut a); let r = a .hits .iter() .find_map(|(r, h)| matches!(h, Hit::DesignTool(2)).then_some(*r)) .unwrap(); click(&mut a, r.x + 1, r.y); click(&mut a, r.x + 1, r.y); assert_eq!(a.design_form().unwrap().root.children.len(), 1); a.execute(Command::Tool("TextBox")); draw(&mut a); let r = a .designer .objects .iter() .find(|(i, _)| *i == root) .unwrap() .1; mouse(&mut a, MK::Down(MB::Left), r.x + 15, r.y + 3, M::NONE); mouse(&mut a, MK::Drag(MB::Left), r.x + 22, r.y + 4, M::NONE); mouse(&mut a, MK::Up(MB::Left), r.x + 22, r.y + 4, M::NONE); assert!(a.dialog.is_none(), "{:?}", a.dialog); let textbox = a.design_form().unwrap().root.children.last().unwrap(); assert_eq!(textbox.class, C::TextBox); assert_eq!(designer::value(textbox, "LEFT"), Some(V::Integer(15))); assert_eq!(designer::value(textbox, "WIDTH"), Some(V::Integer(8))); a.execute(Command::Grid); assert!(a.designer.grid); assert!(draw(&mut a).contains('·')); a.execute(Command::Grid); assert!(!a.designer.grid); let first = a.design_document().unwrap(); a.execute(Command::NewForm); let second = a.design_document().unwrap(); assert_ne!(first, second); let command=a.window_commands().into_iter().find(|(_,c)|matches!(c,Command::FocusWindow(id) if a.windows.iter().any(|w|w.id==*id&&matches!(w.kind,WindowKind::Code(v) if a.project.view(v).unwrap().document()==first)))).unwrap().1; a.execute(command); assert_eq!(a.design_document().unwrap(), first); } #[test] fn binary_designer_import_requires_text_target_and_compiles() { let t = Temp::new(); let bytes: Vec = include_str!("../../tb-ui/tests/data/new.frm.hex") .split_whitespace() .map(|s| u8::from_str_radix(s, 16).unwrap()) .collect(); let source = t.0.join("binary.frm"); fs::write(&source, &bytes).unwrap(); let mut a = App::new(&t.0, t.0.join("options"), (100, 30)).unwrap(); a.load_initial_project(source.clone()).unwrap(); a.execute(Command::Form); plain(&mut a, K::Enter); let root = root(&mut a); a.design_select(root, false).unwrap(); property(&mut a, "CAPTION", "Imported and edited"); let doc = a.design_document().unwrap(); assert!(a.project.save_file(doc, None).is_err()); let path = t.0.join("text.frm"); a.project .save_file(doc, Some(&Destination::new(&path))) .unwrap(); assert_eq!(fs::read(&source).unwrap(), bytes); tb_vm::project_io::load_program(&path).unwrap(); } fn assert_equivalent(a: &frm::FormNode, b: &frm::FormNode) { assert_eq!( (a.class, &a.name, designer::node_key(a)), (b.class, &b.name, designer::node_key(b)) ); for p in forms::properties(a.class) { assert_eq!( designer::value(a, p.name), designer::value(b, p.name), "{}.{}", a.name, p.name ); } assert_eq!(a.children.len(), b.children.len()); for (a, b) in a.children.iter().zip(&b.children) { assert_equivalent(a, b); } } #[test] fn rename_preserves_shadowed_names_and_handles_form_aliases_and_include_calls() { let t = Temp::new(); let mut a = t.app(); let r = root(&mut a); let b = a .design_place(C::CommandButton, 0, r, Rect::new(2, 2, 8, 2)) .unwrap(); let doc = a.design_document().unwrap(); let name = node(&mut a, b).name; let form = node(&mut a, r).name; let code=format!("SUB {name}_Click\nDIM {name}_Click AS INTEGER\n{name}_Click = 7\nPRINT {name}_Click\nEND SUB\nSUB {form}_Load\n{name}.Caption=\"Hi\"\nEND SUB\n"); a.project.replace_text(doc, 0..0, &code).unwrap(); // In eine andere Datei gebundene Aufrufe müssen mit umbenannt werden. let other = a.project.new_module("Calls").unwrap(); let call = format!("DECLARE SUB {name}_Click ()\nCALL {name}_Click\n"); a.project.replace_text(other, 0..0, &call).unwrap(); a.design_rename("Launch").unwrap(); let changed = a.project.document(doc).unwrap().code(); assert!(changed.contains(&format!("DIM {name}_Click AS INTEGER"))); assert!(changed.contains(&format!("PRINT {name}_Click"))); assert!(changed.contains("SUB Launch_CLICK")); assert!(a .project .document(other) .unwrap() .code() .contains("CALL Launch_CLICK")); a.design_select(r, false).unwrap(); a.design_event(r, "LOAD").unwrap(); assert!(!a .project .document(doc) .unwrap() .code() .contains("SUB Form_LOAD")); key(&mut a, K::F(12), M::SHIFT); a.design_select(r, false).unwrap(); a.design_rename("MainForm").unwrap(); assert_eq!(node(&mut a, r).name, "MainForm"); assert!(a .project .document(doc) .unwrap() .code() .contains("SUB MainForm_LOAD")); } #[test] fn existing_default_typed_event_and_all_metadata_signatures_compile() { let t = Temp::new(); let mut a = t.app(); let r = root(&mut a); let b = a .design_place(C::CommandButton, 0, r, Rect::new(2, 2, 8, 2)) .unwrap(); property(&mut a, "INDEX", "0"); let name = node(&mut a, b).name; let doc = a.design_document().unwrap(); let code = format!("DEFINT I,K,S\nSUB {name}_KeyDown (Index, KeyCode, Shift)\nEND SUB\n"); a.project.replace_text(doc, 0..0, &code).unwrap(); a.design_event(b, "KEYDOWN").unwrap(); assert_eq!(a.project.document(doc).unwrap().code(), code); key(&mut a, K::F(12), M::SHIFT); for event in forms::events(C::CommandButton) { a.design_event(b, event).unwrap(); a.compile_current().unwrap(); key(&mut a, K::F(12), M::SHIFT); } } #[test] fn all_eight_handles_resize_transactionally_and_preserve_identity_after_structure_edits() { let t = Temp::new(); let mut a = t.app(); let r = root(&mut a); let b = a .design_place(C::TextBox, 0, r, Rect::new(8, 5, 10, 5)) .unwrap(); for (horizontal, vertical) in [ (-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (0, 1), (1, 1), ] { a.design_select(b, false).unwrap(); draw(&mut a); let rect = a .designer .objects .iter() .find(|(id, _)| *id == b) .unwrap() .1; let x = match horizontal { -1 => rect.x, 0 => rect.x + rect.width / 2, _ => rect.right() - 1, }; let y = match vertical { -1 => rect.y, 0 => rect.y + rect.height / 2, _ => rect.bottom() - 1, }; let before = a.design_form().unwrap().clone(); mouse(&mut a, MK::Down(MB::Left), x, y, M::NONE); mouse(&mut a, MK::Up(MB::Left), x + 1, y + 1, M::NONE); assert!(a.dialog.is_none(), "{:?}", a.dialog); assert_eq!(number(&mut a, b, "WIDTH"), 10 + horizontal); assert_eq!(number(&mut a, b, "HEIGHT"), 5 + vertical); assert_eq!(number(&mut a, b, "LEFT"), 8 + i32::from(horizontal < 0)); assert_eq!(number(&mut a, b, "TOP"), 5 + i32::from(vertical < 0)); a.execute(Command::Undo); assert_eq!(*a.design_form().unwrap(), before); } let old_id = b; a.design_select(b, false).unwrap(); property(&mut a, "INDEX", "0"); assert_eq!(node(&mut a, old_id).class, C::TextBox); a.execute(Command::Undo); assert_eq!(designer::node_key(&node(&mut a, old_id)).1, None); a.design_select(b, false).unwrap(); a.execute(Command::Clear); plain(&mut a, K::Esc); let replacement = a .design_place(C::TextBox, 0, r, Rect::new(1, 1, 5, 2)) .unwrap(); assert_ne!(replacement, b); a.execute(Command::Undo); a.execute(Command::Undo); assert_eq!(node(&mut a, b).class, C::TextBox); } #[test] fn menu_indent_outdent_rename_and_field_changes_have_one_undo() { let t = Temp::new(); let mut a = t.app(); let r = root(&mut a); let title = a.design_place(C::Menu, 0, r, Rect::default()).unwrap(); let child = a.design_place(C::Menu, 0, title, Rect::default()).unwrap(); let sibling = a.design_place(C::Menu, 0, title, Rect::default()).unwrap(); a.design_menu_move("in").unwrap(); assert_eq!(node(&mut a, child).children.len(), 1); a.design_menu_move("out").unwrap(); assert_eq!(node(&mut a, title).children.len(), 2); let before = a.design_form().unwrap().clone(); a.execute(Command::MenuDesign); plain(&mut a, K::Enter); let d = a.dialog.as_mut().unwrap(); d.fields[0] = Field::text("CtlName", "SaveMenu"); d.fields[1] = Field::text("Caption", "&Save"); d.fields[2] = Field::text("Tag", "saved"); d.fields[3] = Field::text("Index", "0"); plain(&mut a, K::Enter); assert!(a.dialog.is_none(), "{:?}", a.dialog); assert_eq!(node(&mut a, sibling).name, "SaveMenu"); a.execute(Command::Undo); assert_eq!(*a.design_form().unwrap(), before); assert_eq!(node(&mut a, sibling).name, "Menu3"); } #[test] fn property_menu_buttons_palette_drag_and_modal_mouse_are_connected() { let t = Temp::new(); let mut a = t.app(); let r = root(&mut a); let before = a.design_form().unwrap().code.clone(); plain(&mut a, K::Char('x')); assert_eq!(a.design_form().unwrap().code, before); draw(&mut a); click(&mut a, 1, 0); assert!(matches!( a.dialog.as_ref().unwrap().kind, DialogKind::DesignProperty )); plain(&mut a, K::Enter); assert!(a.value_focus); plain(&mut a, K::Esc); a.execute(Command::Palette); draw(&mut a); let swatch = a .hits .iter() .find_map(|(r, h)| matches!(h, Hit::DesignPaint("BACKCOLOR", 5)).then_some(*r)) .unwrap(); let form = a .designer .objects .iter() .find(|(id, _)| *id == r) .unwrap() .1; mouse(&mut a, MK::Down(MB::Left), swatch.x, swatch.y, M::NONE); mouse( &mut a, MK::Drag(MB::Left), form.right() - 3, form.bottom() - 3, M::NONE, ); mouse( &mut a, MK::Up(MB::Left), form.right() - 3, form.bottom() - 3, M::NONE, ); assert_eq!(number(&mut a, r, "BACKCOLOR"), 5); a.execute(Command::MenuDesign); draw(&mut a); let insert = a .hits .iter() .find_map(|(r, h)| matches!(h, Hit::DesignAction("insert")).then_some(*r)) .unwrap(); click(&mut a, insert.x, insert.y); assert_eq!(a.design_form().unwrap().root.children.len(), 1); draw(&mut a); let edit = a .hits .iter() .find_map(|(r, h)| matches!(h, Hit::DesignAction("menu")).then_some(*r)) .unwrap(); click(&mut a, edit.x, edit.y); assert!(matches!( a.dialog.as_ref().unwrap().kind, DialogKind::DesignMenu(_) )); draw(&mut a); let before = a.design_form().unwrap().clone(); click(&mut a, 1, 0); assert_eq!(*a.design_form().unwrap(), before); assert!(matches!( a.dialog.as_ref().unwrap().kind, DialogKind::DesignMenu(_) )); } #[test] fn shared_include_ambiguity_aborts_rename_and_event_defaults_use_includes() { let t = Temp::new(); fs::write(t.0.join("shared.bi"), "PRINT Button.Caption\n").unwrap(); for (file, name) in [("a.frm", "AForm"), ("b.frm", "BForm")] { fs::write(t.0.join(file),format!("VERSION 1.00\nBegin Form {name}\n Width=40\n Height=15\n Begin CommandButton Button\n Width=8\n End\nEnd\n'$INCLUDE: 'shared.bi'\n")).unwrap(); } fs::write(t.0.join("p.mak"), "a.frm\nb.frm\n").unwrap(); let mut a = App::new(&t.0, t.0.join("options"), (100, 30)).unwrap(); a.load_initial_project(t.0.join("p.mak")).unwrap(); a.options.syntax_checking = false; a.execute(Command::Form); plain(&mut a, K::Enter); let b = a.design_objects().unwrap()[1].0; a.design_select(b, false).unwrap(); let before: Vec<_> = a .project .documents() .map(|(id, d)| (id, d.content().clone())) .collect(); assert!(a.design_rename("Changed").is_err()); for (id, c) in before { assert_eq!(*a.project.document(id).unwrap().content(), c); } let t = Temp::new(); let mut a = t.app(); let r = root(&mut a); let b = a .design_place(C::CommandButton, 0, r, Rect::new(1, 1, 8, 2)) .unwrap(); fs::write(t.0.join("defaults.bi"), "DEFINT A-Z\n").unwrap(); let doc = a.design_document().unwrap(); a.project .replace_text(doc, 0..0, "'$INCLUDE: 'defaults.bi'\n") .unwrap(); a.design_event(b, "CLICK").unwrap(); assert!(a .project .document(doc) .unwrap() .code() .contains("DEFINT A-Z\n\nEND SUB")); a.compile_current().unwrap(); } #[test] fn imported_type_shadow_is_not_an_object_reference_during_rename() { let t = Temp::new(); let mut a = t.app(); let r = root(&mut a); let b = a .design_place(C::CommandButton, 0, r, Rect::new(1, 1, 8, 2)) .unwrap(); let doc = a.design_document().unwrap(); let name = node(&mut a, b).name; let types = a.project.new_module("Types").unwrap(); a.project .replace_text(types, 0..0, "TYPE Imported\nCaption AS STRING\nEND TYPE\n") .unwrap(); let code = format!( "DIM {name} AS Imported\n{name}.Caption = \"UDT field\"\nSUB {name}_Click\nEND SUB\n" ); a.project.replace_text(doc, 0..0, &code).unwrap(); a.design_rename("RunButton").unwrap(); let code = a.project.document(doc).unwrap().code(); assert!(code.contains(&format!("{name}.Caption = \"UDT field\""))); assert!(code.contains("SUB RunButton_CLICK")); } #[test] fn events_with_the_same_name_are_local_to_their_form() { let t = Temp::new(); let mut a = t.app(); let r1 = root(&mut a); let b1 = a .design_place(C::CommandButton, 0, r1, Rect::new(1, 1, 8, 2)) .unwrap(); let d1 = a.design_document().unwrap(); a.design_event(b1, "CLICK").unwrap(); key(&mut a, K::F(12), M::SHIFT); a.design_event(r1, "LOAD").unwrap(); key(&mut a, K::F(12), M::SHIFT); a.execute(Command::NewForm); let r2 = root(&mut a); let b2 = a .design_place(C::CommandButton, 0, r2, Rect::new(1, 1, 8, 2)) .unwrap(); let d2 = a.design_document().unwrap(); a.design_event(b2, "CLICK").unwrap(); assert_eq!(a.active_document(), Some(d2)); key(&mut a, K::F(12), M::SHIFT); a.design_event(r2, "LOAD").unwrap(); assert_eq!(a.active_document(), Some(d2)); a.compile_current().unwrap(); assert!(a .project .document(d1) .unwrap() .code() .contains("SUB Form_LOAD")); assert!(a .project .document(d2) .unwrap() .code() .contains("SUB Form_LOAD")); } #[test] fn indexed_controls_merge_atomically_with_bindings_and_undo() { let t = Temp::new(); let mut a = t.app(); let root = root(&mut a); let first = a .design_place(C::CommandButton, 0, root, Rect::new(1, 1, 8, 2)) .unwrap(); property(&mut a, "INDEX", "0"); let target = node(&mut a, first).name; let second = a .design_place(C::CommandButton, 0, root, Rect::new(1, 4, 8, 2)) .unwrap(); let old = node(&mut a, second).name; // Scalar and duplicate-index merges must leave every document untouched. let before = a.design_form().unwrap().clone(); assert!(a.design_rename(&target).is_err()); assert_eq!(*a.design_form().unwrap(), before); property(&mut a, "INDEX", "0"); let before = a.design_form().unwrap().clone(); assert!(a.design_rename(&target).is_err()); assert_eq!(*a.design_form().unwrap(), before); property(&mut a, "INDEX", "1"); let unrelated = a .design_place(C::TextBox, 0, root, Rect::new(20, 1, 8, 2)) .unwrap(); let unrelated_name = node(&mut a, unrelated).name; property(&mut a, "INDEX", "2"); let before = a.design_form().unwrap().clone(); assert!(a.design_rename(&target).is_err()); assert_eq!(*a.design_form().unwrap(), before); a.design_select(second, false).unwrap(); let doc = a.design_document().unwrap(); let source = format!("SUB {old}_Click(Index AS INTEGER)\n{old}(1).Caption = {target}(0).Caption\n{unrelated_name}(2).Text = \"kept\"\nEND SUB\n"); a.project.replace_text(doc, 0..0, &source).unwrap(); let before = a.design_form().unwrap().clone(); a.design_rename(&target).unwrap(); assert_eq!(node(&mut a, second).name, target); let code = &a.design_form().unwrap().code; assert!(code .to_uppercase() .contains(&format!("SUB {}_CLICK", target.to_uppercase()))); assert!(code.contains(&format!("{target}(1).Caption"))); assert!(code.contains(&format!("{unrelated_name}(2).Text"))); a.execute(Command::Undo); assert_eq!(*a.design_form().unwrap(), before); // Two handlers for the merged event are ambiguous and cannot be committed. a.project .replace_text( doc, 0..0, &format!("SUB {target}_Click(Index AS INTEGER)\nEND SUB\n"), ) .unwrap(); let before = a.design_form().unwrap().clone(); a.design_select(second, false).unwrap(); assert!(a.design_rename(&target).is_err()); assert_eq!(*a.design_form().unwrap(), before); }