927 lines
30 KiB
Rust
927 lines
30 KiB
Rust
use crossterm::event::{
|
|
Event, KeyCode as K, KeyEvent, KeyModifiers as M, MouseButton, MouseEvent, MouseEventKind,
|
|
};
|
|
use ratatui::{backend::TestBackend, style::Color, Terminal};
|
|
use std::{
|
|
fs,
|
|
path::PathBuf,
|
|
sync::atomic::{AtomicUsize, Ordering},
|
|
};
|
|
use tb_ide::{
|
|
app::{App, DialogKind, Execution, Hit, Mode, WindowKind, WindowState},
|
|
commands::{self, Command},
|
|
export::{ExportStatus, ProjectStamp},
|
|
options::Options,
|
|
};
|
|
|
|
struct Temp(PathBuf);
|
|
impl Temp {
|
|
fn new() -> Self {
|
|
static N: AtomicUsize = AtomicUsize::new(0);
|
|
let path = std::env::temp_dir().join(format!(
|
|
"tb-app-{}-{}",
|
|
std::process::id(),
|
|
N.fetch_add(1, Ordering::Relaxed)
|
|
));
|
|
fs::create_dir_all(&path).unwrap();
|
|
Self(path.canonicalize().unwrap())
|
|
}
|
|
fn app(&self) -> App {
|
|
App::new(&self.0, self.0.join("config/options.ini"), (100, 30)).unwrap()
|
|
}
|
|
}
|
|
impl Drop for Temp {
|
|
fn drop(&mut self) {
|
|
let _ = fs::remove_dir_all(&self.0);
|
|
}
|
|
}
|
|
fn key(app: &mut App, k: K, m: M) {
|
|
app.handle(Event::Key(KeyEvent::new(k, m)));
|
|
}
|
|
fn plain(app: &mut App, k: K) {
|
|
key(app, k, M::NONE);
|
|
}
|
|
fn type_text(app: &mut App, text: &str) {
|
|
for c in text.chars() {
|
|
plain(app, K::Char(c));
|
|
}
|
|
}
|
|
fn field(app: &mut App, index: usize, text: &str) {
|
|
while app.dialog.as_ref().unwrap().focus != index {
|
|
plain(app, K::Tab);
|
|
}
|
|
key(app, K::Char('a'), M::CONTROL);
|
|
plain(app, K::Delete);
|
|
type_text(app, text);
|
|
}
|
|
fn menu(app: &mut App, command: Command) {
|
|
let menus = app.menus();
|
|
let (m, i) = menus
|
|
.iter()
|
|
.enumerate()
|
|
.find_map(|(m, menu)| {
|
|
menu.items
|
|
.iter()
|
|
.position(|i| i.command == Some(command))
|
|
.map(|i| (m, i))
|
|
})
|
|
.unwrap();
|
|
key(app, K::Char(menus[m].mnemonic), M::ALT);
|
|
for _ in 0..i {
|
|
plain(app, K::Down);
|
|
}
|
|
plain(app, K::Enter);
|
|
assert_eq!(app.last_command, Some(command));
|
|
}
|
|
fn draw(app: &mut App) -> (String, ratatui::buffer::Buffer) {
|
|
let mut term = Terminal::new(TestBackend::new(app.size.0, app.size.1)).unwrap();
|
|
term.draw(|f| app.render(f)).unwrap();
|
|
let b = term.backend().buffer().clone();
|
|
let text = b
|
|
.content
|
|
.chunks(app.size.0 as usize)
|
|
.map(|row| row.iter().map(|c| c.symbol()).collect::<String>())
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
(text, b)
|
|
}
|
|
fn click(app: &mut App, x: u16, y: u16) {
|
|
app.handle(Event::Mouse(MouseEvent {
|
|
kind: MouseEventKind::Down(MouseButton::Left),
|
|
column: x,
|
|
row: y,
|
|
modifiers: M::NONE,
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn start_snapshot_and_reference_menus_have_all_commands() {
|
|
let t = Temp::new();
|
|
let mut app = t.app();
|
|
let (text, b) = draw(&mut app);
|
|
assert!(
|
|
text.contains("[1] Untitled")
|
|
&& text.contains("Project: Untitled")
|
|
&& text.contains("Copyright")
|
|
&& text.contains("00001:001")
|
|
);
|
|
assert_eq!(b[(4, 1)].bg, Color::Magenta);
|
|
assert_eq!(b[(4, 1)].fg, Color::White);
|
|
assert_eq!(b[(4, 2)].bg, Color::Blue);
|
|
assert_eq!(b[(1, 29)].bg, Color::Cyan);
|
|
assert_eq!(b[(1, 0)].bg, Color::Gray);
|
|
plain(&mut app, K::Char('x'));
|
|
assert_eq!(
|
|
app.project
|
|
.document(app.active_document().unwrap())
|
|
.unwrap()
|
|
.code(),
|
|
"x"
|
|
);
|
|
assert_eq!(
|
|
commands::menus(false)
|
|
.iter()
|
|
.map(|m| m.title)
|
|
.collect::<Vec<_>>(),
|
|
["File", "Edit", "View", "Search", "Run", "Debug", "Options", "Window", "Help"]
|
|
);
|
|
assert_eq!(
|
|
commands::menus(true)
|
|
.iter()
|
|
.map(|m| m.title)
|
|
.collect::<Vec<_>>(),
|
|
["File", "Edit", "View", "Tools", "Options", "Window", "Help"]
|
|
);
|
|
let reference = include_str!("../../../docs/ide-referenz.md");
|
|
for designer in [false, true] {
|
|
for m in commands::menus(designer) {
|
|
let mut mnemonics = std::collections::BTreeSet::new();
|
|
for item in m.items.iter().filter(|i| i.command.is_some()) {
|
|
let name = item.text();
|
|
assert!(
|
|
reference.contains(name.trim_end_matches('…'))
|
|
|| item.command == Some(Command::Diagnostics)
|
|
|| matches!(item.command, Some(Command::Tool(_))),
|
|
"Nicht in Referenz: {name}"
|
|
);
|
|
assert!(item.mnemonic().is_some());
|
|
assert!(
|
|
mnemonics.insert(item.mnemonic().unwrap()),
|
|
"Doppeltes Mnemonic in {}: {}",
|
|
m.title,
|
|
item.label
|
|
);
|
|
}
|
|
}
|
|
}
|
|
assert_eq!(
|
|
commands::menus(false)[0]
|
|
.items
|
|
.iter()
|
|
.filter_map(|i| i.command)
|
|
.collect::<Vec<_>>(),
|
|
[
|
|
Command::NewProject,
|
|
Command::OpenProject,
|
|
Command::SaveProject,
|
|
Command::NewForm,
|
|
Command::NewModule,
|
|
Command::AddFile,
|
|
Command::RemoveFile,
|
|
Command::SaveFile,
|
|
Command::SaveAs,
|
|
Command::LoadText,
|
|
Command::SaveText,
|
|
Command::Print,
|
|
Command::Shell,
|
|
Command::Exit
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn modal_focus_and_dirty_project_cancel_preserve_document() {
|
|
let t = Temp::new();
|
|
let mut app = t.app();
|
|
type_text(&mut app, "PRINT 7");
|
|
let id = app.active_document().unwrap();
|
|
let active = app.active;
|
|
menu(&mut app, Command::NewModule);
|
|
type_text(&mut app, "Other");
|
|
plain(&mut app, K::F(6));
|
|
key(&mut app, K::Char('c'), M::CONTROL);
|
|
plain(&mut app, K::Esc);
|
|
assert_eq!(app.active, active);
|
|
assert_eq!(app.project.document(id).unwrap().code(), "PRINT 7");
|
|
assert!(app.basic_events.is_empty());
|
|
key(&mut app, K::F(4), M::ALT);
|
|
assert!(matches!(
|
|
app.dialog.as_ref().unwrap().kind,
|
|
DialogKind::Dirty(_)
|
|
));
|
|
plain(&mut app, K::Enter);
|
|
assert!(!app.quit);
|
|
menu(&mut app, Command::NewProject);
|
|
plain(&mut app, K::Right);
|
|
plain(&mut app, K::Enter);
|
|
assert!(matches!(
|
|
app.dialog.as_ref().unwrap().kind,
|
|
DialogKind::Save { .. }
|
|
));
|
|
plain(&mut app, K::Esc);
|
|
assert!(app.project.document(id).unwrap().is_dirty());
|
|
assert!(!app.quit);
|
|
menu(&mut app, Command::SaveProject);
|
|
field(&mut app, 0, &t.0.join("project.mak").display().to_string());
|
|
plain(&mut app, K::Enter);
|
|
assert!(app.dialog.is_none(), "{:?}", app.dialog);
|
|
assert!(!app.project.is_dirty());
|
|
menu(&mut app, Command::Exit);
|
|
assert!(app.quit);
|
|
}
|
|
|
|
#[test]
|
|
fn dispatcher_keeps_function_keys_and_copy_out_of_basic_input() {
|
|
let t = Temp::new();
|
|
let mut app = t.app();
|
|
key(&mut app, K::Char('c'), M::CONTROL);
|
|
assert_eq!(app.last_command, Some(Command::Copy));
|
|
assert!(app.editor.clipboard.is_empty());
|
|
assert!(app.basic_events.is_empty());
|
|
plain(&mut app, K::F(11));
|
|
assert!(app.menu.is_some());
|
|
plain(&mut app, K::Esc);
|
|
menu(&mut app, Command::NewForm);
|
|
let form = app.active_document();
|
|
assert_eq!(app.mode, Mode::Designer);
|
|
plain(&mut app, K::F(2));
|
|
assert!(app.value_focus);
|
|
plain(&mut app, K::F(10));
|
|
assert!(!app.properties && app.menu.is_some());
|
|
plain(&mut app, K::F(10));
|
|
assert!(app.properties && app.menu.is_none());
|
|
plain(&mut app, K::F(12));
|
|
assert_eq!(app.last_command, Some(Command::Events));
|
|
assert_eq!(app.active_document(), form);
|
|
assert!(app.basic_events.is_empty());
|
|
plain(&mut app, K::F(11));
|
|
plain(&mut app, K::Esc);
|
|
menu(&mut app, Command::Code);
|
|
plain(&mut app, K::Enter);
|
|
assert_eq!(app.mode, Mode::Environment);
|
|
app.execution = Execution::Paused;
|
|
plain(&mut app, K::F(10));
|
|
assert_eq!(app.last_command, Some(Command::ProcedureStep));
|
|
assert_eq!(app.availability(Command::ProcedureStep), None);
|
|
menu(&mut app, Command::Output);
|
|
app.execution = Execution::Running;
|
|
plain(&mut app, K::Char('a'));
|
|
assert_eq!(app.basic_events.len(), 1);
|
|
plain(&mut app, K::F(11));
|
|
assert_eq!(app.basic_events.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn windows_restore_geometry_and_resize_without_document_loss() {
|
|
let t = Temp::new();
|
|
let mut app = t.app();
|
|
app.options.syntax_checking = false; // Dieser Test prüft Fenster mit absichtlich ungültigem Entwurf.
|
|
type_text(&mut app, "abc");
|
|
let doc = app.active_document().unwrap();
|
|
let first = app.active;
|
|
menu(&mut app, Command::NewWindow);
|
|
assert_eq!(app.active_document(), Some(doc));
|
|
assert_eq!(app.windows.len(), 3);
|
|
key(&mut app, K::F(8), M::CONTROL);
|
|
plain(&mut app, K::Left);
|
|
plain(&mut app, K::Up);
|
|
plain(&mut app, K::Enter);
|
|
key(&mut app, K::F(7), M::CONTROL);
|
|
plain(&mut app, K::Right);
|
|
plain(&mut app, K::Down);
|
|
plain(&mut app, K::Enter);
|
|
let normal = app.active_window().unwrap().normal;
|
|
key(&mut app, K::F(9), M::CONTROL);
|
|
assert_eq!(app.active_window().unwrap().state, WindowState::Minimized);
|
|
key(&mut app, K::F(10), M::CONTROL);
|
|
assert_eq!(app.active_window().unwrap().state, WindowState::Maximized);
|
|
key(&mut app, K::F(5), M::CONTROL);
|
|
assert_eq!(app.active_window().unwrap().normal, normal);
|
|
let active = app.active;
|
|
app.handle(Event::Resize(40, 12));
|
|
let (small, _) = draw(&mut app);
|
|
assert!(small.contains("mindestens"));
|
|
assert!(!small.contains("Untitled"));
|
|
plain(&mut app, K::Char('z'));
|
|
app.handle(Event::Resize(80, 25));
|
|
draw(&mut app);
|
|
assert_eq!(app.active, active);
|
|
assert_eq!(app.project.document(doc).unwrap().code(), "abc");
|
|
for w in &app.windows {
|
|
let r = app.rect(w);
|
|
assert!(r.right() <= 80 && r.bottom() <= 24);
|
|
}
|
|
key(&mut app, K::Char('-'), M::ALT);
|
|
assert!(app.control_menu);
|
|
plain(&mut app, K::Esc);
|
|
key(&mut app, K::F(4), M::CONTROL);
|
|
assert_eq!(app.windows.len(), 2);
|
|
assert!(app.project.document(doc).is_ok());
|
|
assert_eq!(app.active, first);
|
|
plain(&mut app, K::F(6));
|
|
assert_ne!(app.active, first);
|
|
key(&mut app, K::F(6), M::SHIFT);
|
|
assert_eq!(app.active, first);
|
|
menu(&mut app, Command::Arrange);
|
|
assert_eq!(app.windows[0].normal.right(), app.windows[1].normal.x);
|
|
}
|
|
|
|
#[test]
|
|
fn status_and_project_buttons_use_the_keyboard_command_path() {
|
|
let t = Temp::new();
|
|
let mut app = t.app();
|
|
draw(&mut app);
|
|
let before = app.active;
|
|
click(&mut app, 17, 29);
|
|
assert_eq!(app.last_command, Some(Command::NextWindow));
|
|
assert_ne!(app.active, before);
|
|
let project = app
|
|
.windows
|
|
.iter()
|
|
.find(|w| w.kind == WindowKind::Project)
|
|
.unwrap()
|
|
.clone();
|
|
let r = app.rect(&project);
|
|
draw(&mut app);
|
|
click(&mut app, r.x + 12, r.y + 2);
|
|
assert_eq!(app.last_command, Some(Command::Code));
|
|
plain(&mut app, K::Esc);
|
|
menu(&mut app, Command::NewForm);
|
|
draw(&mut app);
|
|
let (text, _) = draw(&mut app);
|
|
assert!(text.contains("Property:"));
|
|
assert!(!text.contains("<F6=Window>"));
|
|
}
|
|
|
|
#[test]
|
|
fn export_dialogs_validate_and_render_all_results_without_creating_files() {
|
|
let t = Temp::new();
|
|
let mut app = t.app();
|
|
app.export_backend = false;
|
|
let stamp = ProjectStamp::capture(&app.project);
|
|
for command in [Command::MakeExe, Command::MakeLibrary] {
|
|
menu(&mut app, command);
|
|
let output = if command == Command::MakeLibrary {
|
|
1
|
|
} else {
|
|
3
|
|
};
|
|
let (system, architecture) = if command == Command::MakeExe {
|
|
(
|
|
app.dialog.as_ref().unwrap().fields[1].string(),
|
|
app.dialog.as_ref().unwrap().fields[2].string(),
|
|
)
|
|
} else {
|
|
(String::new(), String::new())
|
|
};
|
|
let (text, _) = draw(&mut app);
|
|
assert!(text.contains("Erzeugen: Phase 6"));
|
|
field(&mut app, output, "");
|
|
plain(&mut app, K::Enter);
|
|
assert!(app.dialog.as_ref().unwrap().error.contains("Ausgabepfad"));
|
|
field(
|
|
&mut app,
|
|
output,
|
|
&t.0.join("notthere/out").display().to_string(),
|
|
);
|
|
plain(&mut app, K::Enter);
|
|
assert!(app
|
|
.dialog
|
|
.as_ref()
|
|
.unwrap()
|
|
.error
|
|
.contains("Zielverzeichnis"));
|
|
field(&mut app, output, &t.0.join("bad.tbc").display().to_string());
|
|
plain(&mut app, K::Enter);
|
|
assert!(app.dialog.as_ref().unwrap().error.contains("TBC"));
|
|
let existing = t.0.join("existing.tbl");
|
|
fs::write(&existing, b"keep").unwrap();
|
|
field(&mut app, output, &existing.display().to_string());
|
|
plain(&mut app, K::Enter);
|
|
assert!(app
|
|
.dialog
|
|
.as_ref()
|
|
.unwrap()
|
|
.error
|
|
.contains("Überschreibentscheidung"));
|
|
plain(&mut app, K::Tab);
|
|
plain(&mut app, K::Char(' '));
|
|
plain(&mut app, K::Enter);
|
|
assert_eq!(fs::read(&existing).unwrap(), b"keep");
|
|
assert!(app.dialog.as_ref().unwrap().request.is_some());
|
|
let target = t.0.join("result.tbl");
|
|
field(&mut app, output, &target.display().to_string());
|
|
plain(&mut app, K::Enter);
|
|
let request = app.dialog.as_ref().unwrap().request.clone().unwrap();
|
|
assert_eq!(request.system, system);
|
|
assert_eq!(request.architecture, architecture);
|
|
assert_eq!(request.project, stamp);
|
|
for status in [
|
|
ExportStatus::Running,
|
|
ExportStatus::Success,
|
|
ExportStatus::Failed("Testfehler".into()),
|
|
ExportStatus::Cancelled,
|
|
] {
|
|
app.export_result(&request, status.clone()).unwrap();
|
|
let (text, _) = draw(&mut app);
|
|
assert!(text.contains(&status.text()), "{text}");
|
|
assert!(!target.exists());
|
|
}
|
|
plain(&mut app, K::Esc);
|
|
assert_eq!(ProjectStamp::capture(&app.project), stamp);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn file_picker_save_conflict_and_project_replacement_use_real_events() {
|
|
let t = Temp::new();
|
|
let mut app = t.app();
|
|
let member = t.0.join("existing.bas");
|
|
fs::write(&member, "PRINT 2\n").unwrap();
|
|
menu(&mut app, Command::AddFile);
|
|
plain(&mut app, K::F(2));
|
|
let DialogKind::Browse { entries, .. } = &app.dialog.as_ref().unwrap().kind else {
|
|
panic!("Dateiwahl fehlt")
|
|
};
|
|
let index = entries.iter().position(|p| p == &member).unwrap();
|
|
for _ in 0..index {
|
|
plain(&mut app, K::Right);
|
|
}
|
|
plain(&mut app, K::Enter);
|
|
assert!(matches!(
|
|
app.dialog.as_ref().unwrap().kind,
|
|
DialogKind::AddFile
|
|
));
|
|
plain(&mut app, K::Enter);
|
|
let id = app.active_document().unwrap();
|
|
type_text(&mut app, "'edit\n");
|
|
fs::write(&member, "external\n").unwrap();
|
|
menu(&mut app, Command::SaveFile);
|
|
plain(&mut app, K::Enter);
|
|
assert!(app
|
|
.dialog
|
|
.as_ref()
|
|
.unwrap()
|
|
.error
|
|
.contains("Überschreibentscheidung"));
|
|
assert_eq!(fs::read_to_string(&member).unwrap(), "external\n");
|
|
plain(&mut app, K::Tab);
|
|
plain(&mut app, K::Char(' '));
|
|
plain(&mut app, K::Enter);
|
|
assert!(app.dialog.is_none());
|
|
assert!(!app.project.document(id).unwrap().is_dirty());
|
|
menu(&mut app, Command::LoadText);
|
|
plain(&mut app, K::F(2));
|
|
plain(&mut app, K::Esc);
|
|
assert!(matches!(
|
|
app.dialog.as_ref().unwrap().kind,
|
|
DialogKind::LoadText
|
|
));
|
|
plain(&mut app, K::Esc);
|
|
let path = t.0.join("sub/p.mak");
|
|
fs::create_dir(path.parent().unwrap()).unwrap();
|
|
fs::write(&path, "main.bas\n").unwrap();
|
|
fs::write(path.parent().unwrap().join("main.bas"), "END\n").unwrap();
|
|
menu(&mut app, Command::OpenProject);
|
|
field(&mut app, 0, &path.display().to_string());
|
|
plain(&mut app, K::Enter);
|
|
assert!(matches!(
|
|
app.dialog.as_ref().unwrap().kind,
|
|
DialogKind::Dirty(_)
|
|
));
|
|
plain(&mut app, K::Right);
|
|
plain(&mut app, K::Right);
|
|
plain(&mut app, K::Enter);
|
|
assert!(app.dialog.is_none());
|
|
assert_eq!(app.project.path(), Some(path.as_path()));
|
|
menu(&mut app, Command::NewProject);
|
|
assert!(app.dialog.is_none());
|
|
assert_eq!(app.project.members().len(), 1);
|
|
assert_eq!(app.project.directory(), path.parent().unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn exit_with_partial_save_retains_dialog_and_unsaved_documents() {
|
|
let t = Temp::new();
|
|
fs::write(t.0.join("a.bas"), "END\n").unwrap();
|
|
fs::write(t.0.join("b.bas"), "END\n").unwrap();
|
|
let mak = t.0.join("p.mak");
|
|
fs::write(&mak, "a.bas\nb.bas\n").unwrap();
|
|
let mut app = t.app();
|
|
app.load_initial_project(mak.clone()).unwrap();
|
|
let ids = app.project.members();
|
|
for id in &ids {
|
|
app.project.replace_text(*id, 0..0, "' changed\n").unwrap();
|
|
}
|
|
let b = t.0.join("b.bas");
|
|
let original = fs::metadata(&b).unwrap().permissions();
|
|
let mut perms = original.clone();
|
|
perms.set_readonly(true);
|
|
fs::set_permissions(&b, perms).unwrap();
|
|
key(&mut app, K::F(4), M::ALT);
|
|
plain(&mut app, K::Right);
|
|
plain(&mut app, K::Enter);
|
|
plain(&mut app, K::Enter);
|
|
assert!(app
|
|
.dialog
|
|
.as_ref()
|
|
.unwrap()
|
|
.error
|
|
.contains("schreibgeschützt"));
|
|
assert!(!app.quit);
|
|
assert!(!app.project.document(ids[0]).unwrap().is_dirty());
|
|
assert!(app.project.document(ids[1]).unwrap().is_dirty());
|
|
assert_eq!(fs::read_to_string(&mak).unwrap(), "a.bas\nb.bas\n");
|
|
fs::set_permissions(&b, original).unwrap();
|
|
plain(&mut app, K::Enter);
|
|
assert!(app.quit);
|
|
assert!(!app.project.is_dirty());
|
|
}
|
|
|
|
#[test]
|
|
fn mouse_focus_is_modal_and_program_abort_is_not_editor_copy() {
|
|
let t = Temp::new();
|
|
let mut app = t.app();
|
|
let active = app.active;
|
|
menu(&mut app, Command::NewModule);
|
|
draw(&mut app);
|
|
click(&mut app, 85, 2);
|
|
assert_eq!(app.active, active);
|
|
plain(&mut app, K::Esc);
|
|
menu(&mut app, Command::Output);
|
|
app.execution = Execution::Running;
|
|
key(&mut app, K::Char('c'), M::CONTROL);
|
|
assert_eq!(app.execution, Execution::Paused);
|
|
assert!(app.basic_events.is_empty());
|
|
plain(&mut app, K::Char('x'));
|
|
assert!(
|
|
app.basic_events.is_empty(),
|
|
"Tasten nach Break gehören nicht mehr BASIC"
|
|
);
|
|
app.execution = Execution::Running;
|
|
let r = app.rect(app.active_window().unwrap());
|
|
let mouse = MouseEvent {
|
|
kind: MouseEventKind::Down(MouseButton::Right),
|
|
column: r.x + 2,
|
|
row: r.y + 2,
|
|
modifiers: M::NONE,
|
|
};
|
|
app.handle(Event::Mouse(mouse));
|
|
let mut local = mouse;
|
|
local.column = 1;
|
|
local.row = 1;
|
|
assert_eq!(app.basic_events.last(), Some(&Event::Mouse(local)));
|
|
app.execution = Execution::Idle;
|
|
menu(&mut app, Command::RightMouse);
|
|
plain(&mut app, K::Char(' '));
|
|
plain(&mut app, K::Enter);
|
|
let before = app.last_command;
|
|
app.handle(Event::Mouse(MouseEvent {
|
|
kind: MouseEventKind::Down(MouseButton::Right),
|
|
column: 2,
|
|
row: 2,
|
|
modifiers: M::NONE,
|
|
}));
|
|
assert_eq!(app.last_command, before);
|
|
menu(&mut app, Command::RightMouse);
|
|
plain(&mut app, K::Char(' '));
|
|
plain(&mut app, K::Enter);
|
|
app.handle(Event::Mouse(MouseEvent {
|
|
kind: MouseEventKind::Down(MouseButton::Right),
|
|
column: 2,
|
|
row: 2,
|
|
modifiers: M::NONE,
|
|
}));
|
|
assert_eq!(app.last_command, Some(Command::Topic));
|
|
}
|
|
|
|
#[test]
|
|
fn designer_form_list_and_context_availability_match_the_active_window() {
|
|
let t = Temp::new();
|
|
fs::write(
|
|
t.0.join("Form1.frm"),
|
|
"VERSION 1.00\nBegin Form Existing\nEnd\n",
|
|
)
|
|
.unwrap();
|
|
let mut app = t.app();
|
|
menu(&mut app, Command::NewForm);
|
|
assert_eq!(
|
|
app.project
|
|
.document(app.active_document().unwrap())
|
|
.unwrap()
|
|
.source_path()
|
|
.file_name()
|
|
.unwrap(),
|
|
"Form2.frm"
|
|
);
|
|
let entries = app.window_commands();
|
|
assert_eq!(entries.len(), 1);
|
|
assert!(entries[0].0.starts_with("• Form:"));
|
|
plain(&mut app, K::F(11));
|
|
key(&mut app, K::Char('h'), M::ALT);
|
|
assert_eq!(app.menus()[app.menu.unwrap().0].title, "Help");
|
|
plain(&mut app, K::Esc);
|
|
menu(&mut app, Command::Code);
|
|
plain(&mut app, K::Enter);
|
|
menu(&mut app, Command::Project);
|
|
assert!(app
|
|
.availability(Command::LoadText)
|
|
.unwrap()
|
|
.contains("Codefenster"));
|
|
}
|
|
|
|
#[test]
|
|
fn clicking_project_members_and_dialog_choices_transfers_focus_and_applies_values() {
|
|
let t = Temp::new();
|
|
let mut app = t.app();
|
|
menu(&mut app, Command::NewForm);
|
|
menu(&mut app, Command::Code);
|
|
plain(&mut app, K::Enter);
|
|
menu(&mut app, Command::Project);
|
|
draw(&mut app);
|
|
let r = app.rect(app.active_window().unwrap());
|
|
// Zweites Mitglied ist das Formular; der Listenklick bindet Enter an Project.
|
|
click(&mut app, r.x + 3, r.y + 5);
|
|
assert_eq!(app.active_window().unwrap().kind, WindowKind::Project);
|
|
plain(&mut app, K::Enter);
|
|
assert_eq!(app.mode, Mode::Designer);
|
|
menu(&mut app, Command::RightMouse);
|
|
draw(&mut app);
|
|
let hit = app
|
|
.hits
|
|
.iter()
|
|
.find(|(_, h)| matches!(h, tb_ide::app::Hit::DialogField(0)))
|
|
.unwrap()
|
|
.0;
|
|
click(&mut app, hit.x, hit.y);
|
|
plain(&mut app, K::Enter);
|
|
assert!(!app.options.right_help);
|
|
}
|
|
|
|
#[test]
|
|
fn options_apply_persist_across_bases_and_preserve_unsaved_values_on_error() {
|
|
let t = Temp::new();
|
|
let mut app = t.app();
|
|
menu(&mut app, Command::Display);
|
|
// Titelhintergrund: Palette 5 -> 6, Desktopzeichen und Tabweite.
|
|
while app.dialog.as_ref().unwrap().focus != 7 {
|
|
plain(&mut app, K::Tab);
|
|
}
|
|
plain(&mut app, K::Right);
|
|
field(&mut app, 14, ".");
|
|
field(&mut app, 15, "4");
|
|
plain(&mut app, K::Enter);
|
|
assert!(app.dialog.is_none());
|
|
let (_, b) = draw(&mut app);
|
|
assert_eq!(b[(4, 1)].bg, Color::Yellow);
|
|
assert_eq!(app.options.tab_width, 4);
|
|
let include = t.0.join("includes");
|
|
fs::create_dir(&include).unwrap();
|
|
fs::write(include.join("shared.bi"), "PRINT 42\n").unwrap();
|
|
menu(&mut app, Command::Paths);
|
|
field(&mut app, 2, &include.display().to_string());
|
|
plain(&mut app, K::Enter);
|
|
let id = app.active_document().unwrap();
|
|
app.project
|
|
.replace_text(id, 0..0, "'$INCLUDE: 'shared.bi'\n")
|
|
.unwrap();
|
|
assert!(app.project.sources().unwrap().units[0]
|
|
.segments
|
|
.iter()
|
|
.any(|s| s.text == "PRINT 42\n"));
|
|
menu(&mut app, Command::SyntaxChecking);
|
|
assert!(!app.options.syntax_checking);
|
|
menu(&mut app, Command::SaveOptions);
|
|
plain(&mut app, K::Enter);
|
|
assert!(app.dialog.is_none());
|
|
let elsewhere = t.0.join("elsewhere");
|
|
fs::create_dir(&elsewhere).unwrap();
|
|
let fresh = App::new(&elsewhere, app.config_path.clone(), (80, 25)).unwrap();
|
|
assert_eq!(fresh.options, app.options);
|
|
let mut perms = fs::metadata(&app.config_path).unwrap().permissions();
|
|
let original = perms.clone();
|
|
perms.set_readonly(true);
|
|
fs::set_permissions(&app.config_path, perms).unwrap();
|
|
menu(&mut app, Command::SyntaxChecking);
|
|
menu(&mut app, Command::SaveOptions);
|
|
plain(&mut app, K::Enter);
|
|
assert!(app
|
|
.dialog
|
|
.as_ref()
|
|
.unwrap()
|
|
.error
|
|
.contains("schreibgeschützt"));
|
|
assert_ne!(app.options, app.saved_options);
|
|
fs::set_permissions(&app.config_path, original).unwrap();
|
|
fs::write(
|
|
&app.config_path,
|
|
"version=1\ntab_width=0\ncolor.3=999,5\ndesktop=xx\nfuture=foo\n",
|
|
)
|
|
.unwrap();
|
|
let (defaults, errors, _) = Options::load(&app.config_path);
|
|
assert_eq!(defaults, Options::default());
|
|
assert_eq!(errors.len(), 4);
|
|
}
|
|
|
|
fn await_export(app: &mut App) -> ExportStatus {
|
|
let start = std::time::Instant::now();
|
|
loop {
|
|
app.tick_export();
|
|
let status = app.dialog.as_ref().unwrap().export_status.clone();
|
|
if status != ExportStatus::Running {
|
|
return status;
|
|
}
|
|
assert!(start.elapsed().as_secs() < 10, "Export hängt");
|
|
std::thread::sleep(std::time::Duration::from_millis(2));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn real_library_export_uses_dirty_include_and_mouse_without_runtime_template() {
|
|
let t = Temp::new();
|
|
fs::write(
|
|
t.0.join("lib.bas"),
|
|
"SUB Answer\n'$INCLUDE: 'body.bi'\nEND SUB\n",
|
|
)
|
|
.unwrap();
|
|
fs::write(t.0.join("body.bi"), "PRINT 1\n").unwrap();
|
|
let mut app = t.app();
|
|
app.load_initial_project(t.0.join("lib.bas")).unwrap();
|
|
let include = app.project.find_document(&t.0.join("body.bi")).unwrap();
|
|
app.project
|
|
.replace_text(include, 0..8, "PRINT 42\n")
|
|
.unwrap();
|
|
let stamp = ProjectStamp::capture(&app.project);
|
|
menu(&mut app, Command::MakeLibrary);
|
|
assert_eq!(app.dialog.as_ref().unwrap().fields.len(), 3);
|
|
let out = t.0.join("out.tbl");
|
|
field(&mut app, 1, &out.display().to_string());
|
|
draw(&mut app);
|
|
let r = app
|
|
.hits
|
|
.iter()
|
|
.find(|(_, h)| matches!(h, Hit::DialogSubmit))
|
|
.unwrap()
|
|
.0;
|
|
app.handle(Event::Mouse(MouseEvent {
|
|
kind: MouseEventKind::Down(MouseButton::Left),
|
|
column: r.x,
|
|
row: r.y,
|
|
modifiers: M::NONE,
|
|
}));
|
|
assert_eq!(await_export(&mut app), ExportStatus::Success);
|
|
assert_eq!(ProjectStamp::capture(&app.project), stamp);
|
|
assert!(app.project.document(include).unwrap().is_dirty());
|
|
assert_eq!(
|
|
fs::read_to_string(t.0.join("body.bi")).unwrap(),
|
|
"PRINT 1\n"
|
|
);
|
|
plain(&mut app, K::Esc);
|
|
assert_eq!(app.last_export.as_ref().unwrap().1, ExportStatus::Success);
|
|
fs::remove_file(t.0.join("lib.bas")).unwrap();
|
|
fs::remove_file(t.0.join("body.bi")).unwrap();
|
|
fs::write(t.0.join("consumer.bas"), "CALL Answer\nEND\n").unwrap();
|
|
fs::write(t.0.join("consumer.mak"), "consumer.bas\nout.tbl\n").unwrap();
|
|
app.load_initial_project(t.0.join("consumer.mak")).unwrap();
|
|
app.start_execution().unwrap();
|
|
for n in 0..100 {
|
|
app.tick_execution(n);
|
|
if app.execution == Execution::Ended {
|
|
break;
|
|
}
|
|
}
|
|
assert_eq!(tb_runtime::snapshot::text(app.session.screen()), " 42 \n");
|
|
}
|
|
|
|
#[test]
|
|
fn executable_target_validation_and_missing_template_are_honest() {
|
|
use tb_ide::{
|
|
documents::Destination,
|
|
export::{Artifact, ExportRequest},
|
|
};
|
|
let t = Temp::new();
|
|
let mut app = t.app();
|
|
for (system, arch) in [
|
|
("windows", "aarch64"),
|
|
("macos", "x86_64"),
|
|
("unknown", "x86_64"),
|
|
] {
|
|
assert!(ExportRequest::validate(
|
|
&app.project,
|
|
Artifact::Executable,
|
|
system,
|
|
arch,
|
|
&Destination::new(t.0.join("out"))
|
|
)
|
|
.is_err());
|
|
}
|
|
app.runtime_dir = t.0.join("missing-runtimes");
|
|
menu(&mut app, Command::MakeExe);
|
|
let out = t.0.join("out");
|
|
field(&mut app, 3, &out.display().to_string());
|
|
plain(&mut app, K::Enter);
|
|
let status = await_export(&mut app);
|
|
assert!(
|
|
matches!(&status, ExportStatus::Failed(e) if e.contains("Runtime-Vorlage fehlt")),
|
|
"{status:?}"
|
|
);
|
|
assert!(!out.exists());
|
|
assert!(app.dialog.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn debugger_reports_absent_library_source_and_can_step_back_to_caller() {
|
|
let t = Temp::new();
|
|
let path = t.0.join("lib.bas");
|
|
fs::write(&path, "SUB Answer\nPRINT 42\nEND SUB\n").unwrap();
|
|
let mut compiler = tb_vm::project::ProjectCompiler::default();
|
|
compiler.debug_symbols = true;
|
|
let bytes = tb_vm::project_io::SourceLoader::default()
|
|
.load(&path)
|
|
.unwrap()
|
|
.compile_library(&mut compiler)
|
|
.unwrap()
|
|
.to_tbl()
|
|
.unwrap();
|
|
fs::write(t.0.join("lib.tbl"), bytes).unwrap();
|
|
fs::remove_file(&path).unwrap();
|
|
fs::write(t.0.join("main.bas"), "CALL Answer\nPRINT 9\nEND\n").unwrap();
|
|
fs::write(t.0.join("main.mak"), "main.bas\nlib.tbl\n").unwrap();
|
|
let mut app = t.app();
|
|
app.load_initial_project(t.0.join("main.mak")).unwrap();
|
|
let documents = app.project.documents().count();
|
|
let mut missing = false;
|
|
for _ in 0..10 {
|
|
app.execute(Command::Step);
|
|
for n in 0..100 {
|
|
app.tick_execution(n);
|
|
if app.execution != Execution::Running {
|
|
break;
|
|
}
|
|
}
|
|
if app.message.contains("Bibliotheksquelle nicht verfügbar") {
|
|
missing = true;
|
|
break;
|
|
}
|
|
}
|
|
assert!(missing, "{}", app.message);
|
|
assert_eq!(app.project.documents().count(), documents);
|
|
assert!(!path.exists());
|
|
app.execute(Command::ProcedureStep);
|
|
for n in 0..100 {
|
|
app.tick_execution(n);
|
|
if app.execution != Execution::Running {
|
|
break;
|
|
}
|
|
}
|
|
app.execute(Command::Continue);
|
|
for n in 0..100 {
|
|
app.tick_execution(n);
|
|
if app.execution == Execution::Ended {
|
|
break;
|
|
}
|
|
}
|
|
assert_eq!(app.execution, Execution::Ended, "{}", app.message);
|
|
assert_eq!(
|
|
tb_runtime::snapshot::text(app.session.screen()),
|
|
" 42 \n 9 \n"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn finalization_failure_is_reported_and_preserves_existing_output() {
|
|
let t = Temp::new();
|
|
let mut app = t.app();
|
|
let target = tb_export::Target::MacosArm64;
|
|
app.runtime_dir = t.0.join("runtimes");
|
|
let template = app
|
|
.runtime_dir
|
|
.join(target.triple())
|
|
.join(target.runtime_name());
|
|
fs::create_dir_all(template.parent().unwrap()).unwrap();
|
|
// Header für den negativen Finalisierungstest, kein ausführbarer Zielnachweis.
|
|
let mut header = vec![0; 256];
|
|
header[..4].copy_from_slice(&[0xcf, 0xfa, 0xed, 0xfe]);
|
|
header[4..8].copy_from_slice(&0x0100000cu32.to_le_bytes());
|
|
header[12..16].copy_from_slice(&2u32.to_le_bytes());
|
|
header.extend_from_slice(&tb_export::runtime_marker(target));
|
|
fs::write(&template, header).unwrap();
|
|
tb_export::prepare_template(&template, target).unwrap();
|
|
menu(&mut app, Command::MakeExe);
|
|
for (n, value) in [(1, "macos"), (2, "aarch64")] {
|
|
while app.dialog.as_ref().unwrap().focus != n {
|
|
plain(&mut app, K::Tab);
|
|
}
|
|
while app.dialog.as_ref().unwrap().fields[n].string() != value {
|
|
plain(&mut app, K::Right);
|
|
}
|
|
}
|
|
let out = t.0.join("existing");
|
|
fs::write(&out, b"old").unwrap();
|
|
field(&mut app, 3, &out.display().to_string());
|
|
plain(&mut app, K::Tab);
|
|
plain(&mut app, K::Char(' '));
|
|
plain(&mut app, K::Enter);
|
|
let status = await_export(&mut app);
|
|
assert!(
|
|
matches!(&status,ExportStatus::Failed(e) if e.contains("Finalisierung")),
|
|
"{status:?}"
|
|
);
|
|
assert_eq!(fs::read(&out).unwrap(), b"old");
|
|
assert!(fs::read_dir(&t.0).unwrap().all(|p| !p
|
|
.unwrap()
|
|
.file_name()
|
|
.to_string_lossy()
|
|
.starts_with(".tb-export-")));
|
|
}
|