use rat_salsa::Control;
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::{Position, Rect};
use ratatui_crossterm::crossterm::event::{Event, MouseButton, MouseEventKind};
use crate::about;
use crate::chrome::Target;
use crate::context::AppGlobal;
use crate::diagram_widget;
use crate::dialog;
use crate::error;
use crate::event::AppEvent;
use crate::export;
use crate::export::ExportDialogState;
use crate::frame;
use crate::menu;
use crate::open;
use crate::open::OpenDialogState;
use crate::save;
use crate::save::SaveDialogState;
use crate::shortcuts;
use crate::state::AppState;
use crate::text;
use crate::text::TextEditorState;
use crate::vocabulary::{Action, AppError, Reply};
use monospace_diagram::Shape;
#[cfg(test)]
use monospace_editing::Command;
#[allow(clippy::unnecessary_wraps, reason = "the signature is the framework's")]
pub(crate) fn init(_state: &mut AppState, _global: &mut AppGlobal) -> Result<(), AppError> {
Ok(())
}
#[allow(clippy::unnecessary_wraps, reason = "the signature is the framework's")]
pub(crate) fn render(
area: Rect,
buf: &mut Buffer,
state: &mut AppState,
global: &mut AppGlobal,
) -> Result<(), AppError> {
frame::draw(area, buf, state, global);
Ok(())
}
#[allow(clippy::unnecessary_wraps, reason = "the signature is the framework's")]
pub(crate) fn event(
event: &AppEvent,
state: &mut AppState,
_global: &mut AppGlobal,
) -> Result<Control<AppEvent>, AppError> {
Ok(match event {
AppEvent::Rendered(_) => Control::Continue,
AppEvent::Terminal(Event::Resize(_, _)) => Control::Changed,
AppEvent::Terminal(event) => match route(event, state) {
Reply::Ignored | Reply::Consumed => Control::Continue,
Reply::Repaint => Control::Changed,
Reply::Quit { .. } => Control::Quit,
Reply::Act(action) => {
apply(&action, state);
cleanup(state);
Control::Changed
}
},
})
}
fn route(event: &Event, state: &mut AppState) -> Reply {
let before = shortcuts::handle_always(event)
.or_else(|| text::handle(event, state.text_mut()))
.or_else(|| dialog::handle(event, state.dialog_mut()))
.or_else(|| open::handle(event, state.open_mut()))
.or_else(|| save::handle(event, state.save_mut()))
.or_else(|| export::handle(event, state.export_mut()));
if before != Reply::Ignored {
return before;
}
let target = target_of(event, state);
let moved = state.chrome_mut().move_to(target);
let answer = menu::handle(event, state.chrome_mut())
.or_else(|| {
let (chrome, diagram) = state.chrome_and_diagram();
diagram_widget::handle(event, diagram, chrome.leaf(), target)
})
.or_else(|| shortcuts::handle_unclaimed(event));
if moved { answer.and_repaint() } else { answer }
}
fn target_of(event: &Event, state: &AppState) -> Target {
let Event::Mouse(mouse) = event else {
return Target::Nothing;
};
if mouse.kind != MouseEventKind::Down(MouseButton::Left) {
return Target::Nothing;
}
state.target_at(Position::new(mouse.column, mouse.row))
}
fn apply(action: &Action, state: &mut AppState) {
match action {
Action::Undo => state.diagram_mut().undo(),
Action::Redo => state.diagram_mut().redo(),
Action::Command(command) => {
state.diagram_mut().perform(command.clone());
}
Action::EditText => {
let opened = state
.diagram()
.selected_entry()
.and_then(|(id, shape)| match shape {
Shape::Box { text, .. } => Some((id, text.clone())),
_ => None,
});
if let Some((id, text)) = opened {
state
.text_mut()
.show(TextEditorState::new(id, text.as_ref()));
}
}
Action::About => state.dialog_mut().show(about::open()),
Action::ShowError(message) => state.dialog_mut().show(error::open(message)),
Action::Open => state.open_mut().show(OpenDialogState::new()),
Action::OpenFile(path) => match crate::load(path) {
Ok(session) => *state = AppState::new(session),
Err(error) => apply(&Action::ShowError(error.to_string()), state),
},
Action::New => *state = AppState::new(crate::empty_session()),
Action::OpenDemo => match crate::demo() {
Ok(session) => *state = AppState::new(session),
Err(error) => apply(&Action::ShowError(error.to_string()), state),
},
Action::Save => state.save_mut().show(SaveDialogState::new()),
Action::SaveFile(path) => {
if let Err(error) = crate::save(path, state.diagram().session()) {
apply(&Action::ShowError(error.to_string()), state);
}
}
Action::ExportText => state.export_mut().show(ExportDialogState::new()),
Action::ExportTextFile(path) => {
if let Err(error) = crate::export_text(path, state.diagram().session()) {
apply(&Action::ShowError(error.to_string()), state);
}
}
}
}
fn cleanup(state: &mut AppState) {
state.chrome_mut().close();
}
#[allow(clippy::unnecessary_wraps, reason = "the signature is the framework's")]
pub(crate) fn error(
error: AppError,
_state: &mut AppState,
global: &mut AppGlobal,
) -> Result<Control<AppEvent>, AppError> {
global.set_failure(error);
Ok(Control::Quit)
}
#[cfg(test)]
mod tests {
use std::num::NonZeroU16;
use super::*;
use crate::chrome::Focus;
use crate::config::Config;
use crate::status;
use crate::vocabulary::AppError;
use monospace_core::{Offset, Pos};
use monospace_diagram::{ShapeId, Text};
use ratatui_core::layout::Rect;
use ratatui_core::style::Modifier;
use ratatui_crossterm::crossterm::event::{
Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
};
const WIDTH: u16 = 40;
const HEIGHT: u16 = 10;
fn a_state() -> AppState {
AppState::new(crate::empty_session())
}
fn a_global() -> AppGlobal {
AppGlobal::new(Config::default(), monospace_glyph_sets::all())
}
const A_BOX: &str = r#"{
"next_id": 2,
"shapes": [ { "kind": "box", "id": 1, "at": { "x": 2, "y": 1 },
"size": { "width": 4, "height": 3 }, "stroke": "light" } ]
}"#;
const A_BOX_WITH_TEXT: &str = r#"{
"next_id": 2,
"shapes": [ { "kind": "box", "id": 1, "at": { "x": 2, "y": 1 },
"size": { "width": 4, "height": 3 }, "stroke": "light",
"text": "hi" } ]
}"#;
const A_CONNECTOR: &str = r#"{
"next_id": 2,
"shapes": [ { "kind": "connector", "id": 1,
"from": { "at": { "kind": "point", "x": 2, "y": 1 },
"leaving": "right", "terminal": { "kind": "arm" } },
"to": { "at": { "kind": "point", "x": 10, "y": 1 },
"leaving": "left", "terminal": { "kind": "arm" } },
"stroke": "light" } ]
}"#;
const A_BOX_AT_ITS_SMALLEST: &str = r#"{
"next_id": 2,
"shapes": [ { "kind": "box", "id": 1, "at": { "x": 2, "y": 1 },
"size": { "width": 2, "height": 2 }, "stroke": "light" } ]
}"#;
const A_HORIZONTAL_LINE: &str = r#"{
"next_id": 2,
"shapes": [ { "kind": "line", "id": 1, "at": { "x": 2, "y": 1 },
"len": 6, "orientation": "horizontal", "stroke": "light" } ]
}"#;
const A_VERTICAL_LINE: &str = r#"{
"next_id": 2,
"shapes": [ { "kind": "line", "id": 1, "at": { "x": 2, "y": 1 },
"len": 3, "orientation": "vertical", "stroke": "light" } ]
}"#;
const A_LINE_AT_ITS_MINIMUM: &str = r#"{
"next_id": 2,
"shapes": [ { "kind": "line", "id": 1, "at": { "x": 2, "y": 1 },
"len": 1, "orientation": "horizontal", "stroke": "light" } ]
}"#;
const A_ZERO_LENGTH_LINE: &str = r#"{
"next_id": 2,
"shapes": [ { "kind": "line", "id": 1, "at": { "x": 2, "y": 1 },
"len": 0, "orientation": "horizontal", "stroke": "light" } ]
}"#;
const A_BOX_AND_WHAT_HANGS_FROM_IT: &str = r#"{
"next_id": 4,
"shapes": [
{ "kind": "box", "id": 1, "at": { "x": 0, "y": 1 },
"size": { "width": 4, "height": 3 }, "stroke": "light" },
{ "kind": "box", "id": 2, "at": { "x": 16, "y": 1 },
"size": { "width": 4, "height": 3 }, "stroke": "light" },
{ "kind": "connector", "id": 3,
"from": { "at": { "kind": "reference", "shape": 1, "anchor": "right" },
"terminal": { "kind": "arm" } },
"to": { "at": { "kind": "reference", "shape": 2, "anchor": "left" },
"terminal": { "kind": "arm" } },
"stroke": "light" }
]
}"#;
const A_BOX_TOO_BIG: &str = r#"{
"next_id": 2,
"shapes": [ { "kind": "box", "id": 1, "at": { "x": 0, "y": 0 },
"size": { "width": 60, "height": 14 }, "stroke": "light" } ]
}"#;
const BOXES_APART: &str = r#"{
"next_id": 4,
"shapes": [
{ "kind": "box", "id": 1, "at": { "x": 0, "y": 0 },
"size": { "width": 4, "height": 3 }, "stroke": "light" },
{ "kind": "box", "id": 2, "at": { "x": 27, "y": 0 },
"size": { "width": 3, "height": 3 }, "stroke": "light" },
{ "kind": "box", "id": 3, "at": { "x": 50, "y": 0 },
"size": { "width": 3, "height": 3 }, "stroke": "light" }
]
}"#;
fn a_state_over(json: &str) -> AppState {
let diagram =
monospace_description::parse(json).expect("the test's description is readable");
AppState::new(monospace_editing::Session::new(diagram))
}
fn a_drawn_state_over(json: &str) -> AppState {
let mut state = a_state_over(json);
let _ = draw(&mut state);
state
}
fn click(column: u16, row: u16) -> Event {
press(MouseButton::Left, column, row)
}
fn press(button: MouseButton, column: u16, row: u16) -> Event {
Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(button),
column,
row,
modifiers: KeyModifiers::NONE,
})
}
fn wheel(kind: MouseEventKind, column: u16, row: u16) -> Event {
wheel_with(kind, KeyModifiers::NONE, column, row)
}
fn wheel_with(kind: MouseEventKind, modifiers: KeyModifiers, column: u16, row: u16) -> Event {
Event::Mouse(MouseEvent {
kind,
column,
row,
modifiers,
})
}
fn drag(column: u16, row: u16) -> Event {
Event::Mouse(MouseEvent {
kind: MouseEventKind::Drag(MouseButton::Left),
column,
row,
modifiers: KeyModifiers::NONE,
})
}
fn moved(column: u16, row: u16) -> Event {
Event::Mouse(MouseEvent {
kind: MouseEventKind::Moved,
column,
row,
modifiers: KeyModifiers::NONE,
})
}
fn a_drawn_state() -> AppState {
let mut state = a_state();
let _ = draw(&mut state);
state
}
fn draw(state: &mut AppState) -> ratatui_core::buffer::Buffer {
let area = Rect::new(0, 0, WIDTH, HEIGHT);
let mut buffer = ratatui_core::buffer::Buffer::empty(area);
frame::draw(area, &mut buffer, state, &a_global());
buffer
}
fn key(code: KeyCode) -> Event {
Event::Key(KeyEvent::new(code, KeyModifiers::NONE))
}
fn key_with(code: KeyCode, modifiers: KeyModifiers) -> Event {
Event::Key(KeyEvent::new(code, modifiers))
}
fn undo_key() -> Event {
key_with(KeyCode::Char('z'), KeyModifiers::CONTROL)
}
fn redo_key() -> Event {
key_with(
KeyCode::Char('Z'),
KeyModifiers::CONTROL | KeyModifiers::SHIFT,
)
}
fn row_of(buffer: &Buffer, line: u16) -> String {
(buffer.area.left()..buffer.area.right())
.map(|column| buffer[(column, line)].symbol().to_string())
.collect()
}
fn reversed(buffer: &Buffer, column: u16, line: u16) -> bool {
buffer[(column, line)]
.style()
.add_modifier
.contains(Modifier::REVERSED)
}
fn send(raw: &Event, state: &mut AppState) -> Control<AppEvent> {
event(&AppEvent::Terminal(raw.clone()), state, &mut a_global())
.expect("the event reached the application")
}
fn open_the_drop_down(state: &mut AppState, label: char) {
let outcome = send(&key_with(KeyCode::Char(label), KeyModifiers::ALT), state);
let _ = draw(state);
assert!(
matches!(outcome, Control::Changed),
"Alt+{label} did not owe a frame: {outcome:?}"
);
assert_eq!(
state.chrome().leaf(),
Focus::Bar,
"Alt+{label} did not give the bar the keyboard"
);
assert!(
state.chrome().popup_active(),
"Alt+{label} did not open the drop-down"
);
}
fn take_the_entry(state: &mut AppState, menu: char, text: &str) -> Control<AppEvent> {
open_the_drop_down(state, menu);
let buffer = draw(state);
let (column, row) = frame::cell_of(&buffer, text);
let _ = send(&moved(column, row), state);
send(&click(column, row), state)
}
fn open_the_about_dialog(state: &mut AppState) {
let outcome = take_the_entry(state, 'h', "About");
assert!(
matches!(outcome, Control::Changed),
"taking About owed no frame: {outcome:?}"
);
assert!(state.dialog().is_up(), "taking About opened no dialog");
assert_eq!(
state.chrome().leaf(),
Focus::Region,
"taking About left the keyboard on the bar"
);
assert!(
!state.chrome().popup_active(),
"taking About left the drop-down open"
);
}
fn open_the_open_dialog(state: &mut AppState) {
let outcome = take_the_entry(state, 'f', "Open");
assert!(
matches!(outcome, Control::Changed),
"taking Open owed no frame: {outcome:?}"
);
assert!(state.open().is_up(), "taking Open opened no dialog");
assert_eq!(
state.chrome().leaf(),
Focus::Region,
"taking Open left the keyboard on the bar"
);
assert!(
!state.chrome().popup_active(),
"taking Open left the drop-down open"
);
}
fn open_the_save_dialog(state: &mut AppState) {
let outcome = take_the_entry(state, 'f', "Save");
assert!(
matches!(outcome, Control::Changed),
"taking Save owed no frame: {outcome:?}"
);
assert!(state.save().is_up(), "taking Save opened no dialog");
assert_eq!(
state.chrome().leaf(),
Focus::Region,
"taking Save left the keyboard on the bar"
);
assert!(
!state.chrome().popup_active(),
"taking Save left the drop-down open"
);
}
fn open_the_export_dialog(state: &mut AppState) {
let outcome = take_the_entry(state, 'f', "Export as Text");
assert!(
matches!(outcome, Control::Changed),
"taking Export as Text owed no frame: {outcome:?}"
);
assert!(
state.export().is_up(),
"taking Export as Text opened no dialog"
);
assert_eq!(
state.chrome().leaf(),
Focus::Region,
"taking Export as Text left the keyboard on the bar"
);
assert!(
!state.chrome().popup_active(),
"taking Export as Text left the drop-down open"
);
}
fn write_description(name: &str, contents: &str) -> std::path::PathBuf {
use std::io::Write as _;
let path =
std::env::temp_dir().join(format!("monospace-open-{name}-{}.json", std::process::id()));
std::fs::File::create(&path)
.and_then(|mut file| file.write_all(contents.as_bytes()))
.expect("temp description file should be writable");
path
}
#[test]
fn the_about_action_changes_no_session() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
assert!(
selected.is_some(),
"the box was not selected, so the test proves nothing"
);
open_the_about_dialog(&mut state);
assert_eq!(
state.diagram().selected(),
selected,
"the dialog changed the selection"
);
assert!(
!state.diagram().can_undo(),
"opening the dialog recorded a step"
);
assert!(
!state.diagram().can_redo(),
"opening the dialog left a step ahead"
);
assert!(
state.diagram().owner(Offset { x: 2, y: 1 }).is_some(),
"opening the dialog removed the box"
);
}
#[test]
fn showing_an_error_opens_the_dialog_over_its_text_and_changes_no_session() {
let mut state = a_drawn_state_over(A_BOX);
open_the_about_dialog(&mut state);
apply(&Action::ShowError("Something failed".into()), &mut state);
assert!(state.dialog().is_up(), "the error left the dialog down");
let mut buffer = Buffer::empty(Rect::new(0, 0, 40, 10));
crate::dialog::draw(
Rect::new(0, 1, 40, 8),
&mut buffer,
state.dialog_mut(),
&Config::default().theme,
);
let frame = crate::dialog::testing::picture(&buffer);
assert!(
frame.contains("Something failed"),
"the dialog does not carry the error: {frame:?}"
);
assert!(
!frame.contains("About"),
"the error did not replace the About: {frame:?}"
);
assert!(
!state.diagram().can_undo(),
"showing the error recorded a step"
);
}
#[test]
fn the_new_action_is_reached_by_the_shortcut_and_by_the_menus_entry() {
let mut state = a_drawn_state_over(A_BOX);
let outcome = send(
&key_with(KeyCode::Char('n'), KeyModifiers::CONTROL),
&mut state,
);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(
state.diagram().session().diagram().shapes().count(),
0,
"the shortcut did not reset to a blank diagram"
);
let mut state = a_drawn_state_over(A_BOX);
let outcome = take_the_entry(&mut state, 'f', "New");
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(
state.diagram().session().diagram().shapes().count(),
0,
"the menu entry did not reset to a blank diagram"
);
}
#[test]
fn the_open_dialog_is_reached_by_the_shortcut_and_by_the_menus_entry() {
let mut state = a_drawn_state();
let outcome = send(
&key_with(KeyCode::Char('o'), KeyModifiers::CONTROL),
&mut state,
);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(state.open().is_up(), "the shortcut did not open the dialog");
let mut state = a_drawn_state();
open_the_open_dialog(&mut state);
assert!(
state.open().is_up(),
"the menu entry did not open the dialog"
);
}
#[test]
fn the_save_dialog_is_reached_by_the_shortcut_and_by_the_menus_entry() {
let mut state = a_drawn_state();
let outcome = send(
&key_with(KeyCode::Char('s'), KeyModifiers::CONTROL),
&mut state,
);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(
state.save().is_up(),
"the shortcut did not open the save dialog"
);
let mut state = a_drawn_state();
open_the_save_dialog(&mut state);
assert!(
state.save().is_up(),
"the menu entry did not open the save dialog"
);
}
#[test]
fn escape_closes_the_about_dialog_and_the_event_reaches_nothing_else() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
open_the_about_dialog(&mut state);
assert!(state.dialog().is_up(), "the dialog is not open");
let focus = state.chrome().leaf();
let outcome = send(&key(KeyCode::Esc), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(!state.dialog().is_up(), "escape did not close the dialog");
assert_eq!(
state.chrome().leaf(),
focus,
"the closing event moved the keyboard"
);
assert!(
!state.chrome().popup_active(),
"the closing event opened a drop-down"
);
assert_eq!(
state.diagram().selected(),
selected,
"the closing event changed the selection"
);
assert!(
!state.diagram().can_undo(),
"the closing event recorded a step"
);
}
#[test]
fn opening_the_open_dialog_changes_no_session() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
assert!(
selected.is_some(),
"the box was not selected, so the test proves nothing"
);
open_the_open_dialog(&mut state);
assert_eq!(
state.diagram().selected(),
selected,
"the dialog changed the selection"
);
assert!(
!state.diagram().can_undo(),
"opening the dialog recorded a step"
);
assert!(
!state.diagram().can_redo(),
"opening the dialog left a step ahead"
);
assert!(
state.diagram().owner(Offset { x: 2, y: 1 }).is_some(),
"opening the dialog removed the box"
);
}
#[test]
fn opening_the_save_dialog_changes_no_session() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
assert!(
selected.is_some(),
"the box was not selected, so the test proves nothing"
);
open_the_save_dialog(&mut state);
assert_eq!(
state.diagram().selected(),
selected,
"the dialog changed the selection"
);
assert!(
!state.diagram().can_undo(),
"opening the dialog recorded a step"
);
assert!(
!state.diagram().can_redo(),
"opening the dialog left a step ahead"
);
assert!(
state.diagram().owner(Offset { x: 2, y: 1 }).is_some(),
"opening the dialog removed the box"
);
}
#[test]
fn escape_closes_the_open_dialog_and_the_event_reaches_nothing_else() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
open_the_open_dialog(&mut state);
assert!(state.open().is_up(), "the open dialog is not open");
let focus = state.chrome().leaf();
let outcome = send(&key(KeyCode::Esc), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(
!state.open().is_up(),
"escape did not close the open dialog"
);
assert_eq!(
state.chrome().leaf(),
focus,
"the closing event moved the keyboard"
);
assert!(
!state.chrome().popup_active(),
"the closing event opened a drop-down"
);
assert_eq!(
state.diagram().selected(),
selected,
"the closing event changed the selection"
);
assert!(
!state.diagram().can_undo(),
"the closing event recorded a step"
);
}
#[test]
fn escape_closes_the_save_dialog_and_the_event_reaches_nothing_else() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
open_the_save_dialog(&mut state);
assert!(state.save().is_up(), "the save dialog is not open");
let focus = state.chrome().leaf();
let outcome = send(&key(KeyCode::Esc), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(
!state.save().is_up(),
"escape did not close the save dialog"
);
assert_eq!(
state.chrome().leaf(),
focus,
"the closing event moved the keyboard"
);
assert!(
!state.chrome().popup_active(),
"the closing event opened a drop-down"
);
assert_eq!(
state.diagram().selected(),
selected,
"the closing event changed the selection"
);
assert!(
!state.diagram().can_undo(),
"the closing event recorded a step"
);
}
#[test]
fn control_q_does_nothing_while_the_open_dialog_is_up() {
let mut state = a_drawn_state_over(A_BOX);
open_the_open_dialog(&mut state);
let (column, row) = frame::cell_of(&draw(&mut state), "File");
let focus = state.chrome().leaf();
let outcome = send(
&key_with(KeyCode::Char('q'), KeyModifiers::CONTROL),
&mut state,
);
assert!(
matches!(outcome, Control::Continue),
"the way out was answered through the open dialog: {outcome:?}"
);
assert!(state.open().is_up(), "Ctrl+Q closed the open dialog");
let _ = send(&click(column, row), &mut state);
assert_eq!(
state.chrome().leaf(),
focus,
"the click on the bar moved the keyboard"
);
assert!(
!state.chrome().popup_active(),
"the click behind the dialog opened a drop-down"
);
assert!(
state.open().is_up(),
"the click on the bar closed the open dialog"
);
}
#[test]
fn control_q_does_nothing_while_the_save_dialog_is_up() {
let mut state = a_drawn_state_over(A_BOX);
open_the_save_dialog(&mut state);
let (column, row) = frame::cell_of(&draw(&mut state), "File");
let focus = state.chrome().leaf();
let outcome = send(
&key_with(KeyCode::Char('q'), KeyModifiers::CONTROL),
&mut state,
);
assert!(
matches!(outcome, Control::Continue),
"the way out was answered through the save dialog: {outcome:?}"
);
assert!(state.save().is_up(), "Ctrl+Q closed the save dialog");
let _ = send(&click(column, row), &mut state);
assert_eq!(
state.chrome().leaf(),
focus,
"the click on the bar moved the keyboard"
);
assert!(
!state.chrome().popup_active(),
"the click behind the save dialog opened a drop-down"
);
assert!(
state.save().is_up(),
"the click on the bar closed the save dialog"
);
}
#[test]
fn a_file_that_loads_starts_a_new_session() {
let path = write_description(
"loaded",
r#"{ "next_id": 2, "shapes": [ { "kind": "box", "id": 1, "at": { "x": 8, "y": 2 },
"size": { "width": 3, "height": 2 }, "stroke": "light" } ] }"#,
);
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
assert!(
state.diagram().selected().is_some(),
"the box was not selected, so the test proves nothing"
);
let _ = send(&key(KeyCode::Delete), &mut state);
assert!(
state.diagram().can_undo(),
"the removal recorded no step, so the test proves nothing"
);
open_the_open_dialog(&mut state);
state
.open_mut()
.get_mut()
.expect("the open dialog is up")
.set_path(path.to_str().expect("the temp path is UTF-8"));
let outcome = send(&key(KeyCode::Enter), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(!state.open().is_up(), "the load left the dialog up");
assert_eq!(
state.chrome().leaf(),
Focus::Region,
"the load moved the keyboard off the region"
);
assert_eq!(
state.diagram().selected(),
None,
"the new session kept the old selection"
);
assert!(
!state.diagram().can_undo(),
"the new session kept the old history"
);
assert!(
!state.diagram().can_redo(),
"the new session kept the old history"
);
let _ = draw(&mut state);
assert!(
state.diagram().owner(Offset { x: 2, y: 1 }).is_none(),
"the old diagram is still there"
);
assert!(
state.diagram().owner(Offset { x: 8, y: 2 }).is_some(),
"the file's box is not there"
);
}
#[test]
fn taking_new_discards_current_session_and_starts_blank() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
assert!(
state.diagram().selected().is_some(),
"the box was not selected, so the test proves nothing"
);
let _ = send(&key(KeyCode::Delete), &mut state);
assert!(
state.diagram().can_undo(),
"the removal recorded no step, so the test proves nothing"
);
let outcome = take_the_entry(&mut state, 'f', "New");
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(
state.chrome().leaf(),
Focus::Region,
"taking New moved the keyboard off the region"
);
assert_eq!(
state.diagram().selected(),
None,
"the new session kept the old selection"
);
assert!(
!state.diagram().can_undo(),
"the new session kept the old history"
);
assert!(
!state.diagram().can_redo(),
"the new session kept the old history"
);
let _ = draw(&mut state);
assert!(
state.diagram().owner(Offset { x: 2, y: 1 }).is_none(),
"the old diagram is still there"
);
assert_eq!(
state.diagram().session().diagram().shapes().count(),
0,
"the blank diagram is not empty"
);
}
#[test]
fn taking_open_demo_discards_current_session_and_loads_demo() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
assert!(
state.diagram().selected().is_some(),
"the box was not selected, so the test proves nothing"
);
let _ = send(&key(KeyCode::Delete), &mut state);
assert!(
state.diagram().can_undo(),
"the removal recorded no step, so the test proves nothing"
);
let outcome = take_the_entry(&mut state, 'f', "Open Demo");
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(
state.chrome().leaf(),
Focus::Region,
"taking Open Demo moved the keyboard off the region"
);
assert_eq!(
state.diagram().selected(),
None,
"the new session kept the old selection"
);
assert!(
!state.diagram().can_undo(),
"the new session kept the old history"
);
assert!(
!state.diagram().can_redo(),
"the new session kept the old history"
);
let _ = draw(&mut state);
assert!(
state.diagram().session().diagram().shapes().count() > 0,
"the demonstration has no shapes"
);
}
#[test]
fn taking_new_or_open_demo_leaves_the_keyboard_on_the_region() {
let mut state = a_drawn_state_over(A_BOX);
let _ = take_the_entry(&mut state, 'f', "New");
assert_eq!(state.chrome().leaf(), Focus::Region);
assert!(!state.chrome().popup_active());
let mut state = a_drawn_state_over(A_BOX);
open_the_drop_down(&mut state, 'f');
assert_eq!(state.chrome().leaf(), Focus::Bar);
let _ = send(
&key_with(KeyCode::Char('n'), KeyModifiers::CONTROL),
&mut state,
);
assert_eq!(state.chrome().leaf(), Focus::Region);
assert!(!state.chrome().popup_active());
let mut state = a_drawn_state_over(A_BOX);
let _ = take_the_entry(&mut state, 'f', "Open Demo");
assert_eq!(state.chrome().leaf(), Focus::Region);
assert!(!state.chrome().popup_active());
}
#[test]
fn a_missing_file_closes_the_open_dialog_and_opens_the_error_dialog() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
open_the_open_dialog(&mut state);
state
.open_mut()
.get_mut()
.expect("the open dialog is up")
.set_path("missing.json");
let outcome = send(&key(KeyCode::Enter), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(!state.open().is_up(), "the failure left the open dialog up");
assert!(
state.dialog().is_up(),
"the failure did not open the error dialog"
);
assert_eq!(
state.diagram().selected(),
selected,
"the failure changed the selection"
);
assert!(!state.diagram().can_undo(), "the failure recorded a step");
assert!(
state.diagram().owner(Offset { x: 2, y: 1 }).is_some(),
"the failure removed the box"
);
}
#[test]
fn a_file_that_does_not_parse_closes_the_open_dialog_and_opens_the_error_dialog() {
let path = write_description("bad", "not json");
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
open_the_open_dialog(&mut state);
state
.open_mut()
.get_mut()
.expect("the open dialog is up")
.set_path(path.to_str().expect("the temp path is UTF-8"));
let outcome = send(&key(KeyCode::Enter), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(!state.open().is_up(), "the failure left the open dialog up");
assert!(
state.dialog().is_up(),
"the failure did not open the error dialog"
);
assert_eq!(
state.diagram().selected(),
selected,
"the failure changed the selection"
);
assert!(!state.diagram().can_undo(), "the failure recorded a step");
assert!(
state.diagram().owner(Offset { x: 2, y: 1 }).is_some(),
"the failure removed the box"
);
}
#[test]
fn a_diagram_is_saved_to_a_file_and_overwrites_existing_content() {
let path = std::env::temp_dir().join(format!(
"monospace-save-overwrite-{}.json",
std::process::id()
));
let path_str = path.to_str().expect("temp path is UTF-8");
std::fs::write(&path, "old content").expect("temp file is writable");
let mut state = a_drawn_state_over(A_BOX);
open_the_save_dialog(&mut state);
state
.save_mut()
.get_mut()
.expect("the save dialog is up")
.set_path(path_str);
let outcome = send(&key(KeyCode::Enter), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(!state.save().is_up(), "saving left the save dialog up");
assert_eq!(
state.chrome().leaf(),
Focus::Region,
"saving moved the keyboard off the region"
);
let read = std::fs::read_to_string(&path).expect("saved file can be read");
assert_ne!(
read, "old content",
"saving did not overwrite existing content"
);
let parsed = monospace_description::parse(&read).expect("saved JSON is valid description");
assert_eq!(
parsed.shapes().count(),
1,
"saved diagram is missing the box"
);
let _ = std::fs::remove_file(path);
}
#[test]
fn saving_leaves_the_current_session_and_history_intact() {
let path =
std::env::temp_dir().join(format!("monospace-save-intact-{}.json", std::process::id()));
let path_str = path.to_str().expect("temp path is UTF-8");
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
assert!(selected.is_some(), "the box was not selected");
let _ = send(&key(KeyCode::Right), &mut state);
assert!(state.diagram().can_undo(), "movement created no undo step");
open_the_save_dialog(&mut state);
state
.save_mut()
.get_mut()
.expect("the save dialog is up")
.set_path(path_str);
let outcome = send(&key(KeyCode::Enter), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(!state.save().is_up(), "saving left the save dialog up");
assert_eq!(
state.diagram().selected(),
selected,
"saving changed the selection"
);
assert!(state.diagram().can_undo(), "saving dropped the undo stack");
assert!(!state.diagram().can_redo(), "saving modified redo stack");
let _ = std::fs::remove_file(path);
}
#[test]
fn an_empty_path_closes_the_save_dialog_and_opens_the_error_dialog() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
open_the_save_dialog(&mut state);
assert_eq!(
state.save().get().expect("the save dialog is up").path(),
""
);
let outcome = send(&key(KeyCode::Enter), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(!state.save().is_up(), "the failure left the save dialog up");
assert!(
state.dialog().is_up(),
"the failure did not open the error dialog"
);
assert_eq!(
state.diagram().selected(),
selected,
"the failure changed the selection"
);
assert!(!state.diagram().can_undo(), "the failure recorded a step");
let area = Rect::new(0, 0, WIDTH, 12);
let mut buffer = Buffer::empty(area);
frame::draw(area, &mut buffer, &mut state, &a_global());
let shown: String = (0..12).map(|line| row_of(&buffer, line)).collect();
assert!(
shown.contains(": a file name is needed"),
"the error dialog does not report the empty path message: {shown:?}"
);
}
#[test]
fn a_path_in_a_missing_directory_closes_the_save_dialog_and_opens_the_error_dialog() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
open_the_save_dialog(&mut state);
let missing_path = "nonexistent_dir_12345/saved.json";
state
.save_mut()
.get_mut()
.expect("the save dialog is up")
.set_path(missing_path);
let outcome = send(&key(KeyCode::Enter), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(!state.save().is_up(), "the failure left the save dialog up");
assert!(
state.dialog().is_up(),
"the failure did not open the error dialog"
);
assert_eq!(
state.diagram().selected(),
selected,
"the failure changed the selection"
);
assert!(!state.diagram().can_undo(), "the failure recorded a step");
let area = Rect::new(0, 0, WIDTH, 12);
let mut buffer = Buffer::empty(area);
frame::draw(area, &mut buffer, &mut state, &a_global());
let shown: String = (0..12).map(|line| row_of(&buffer, line)).collect();
assert!(
shown.contains("nonexistent_dir_12345"),
"the error dialog does not name the missing directory: {shown:?}"
);
}
#[test]
fn opening_the_export_dialog_changes_no_session() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
assert!(
selected.is_some(),
"the box was not selected, so the test proves nothing"
);
open_the_export_dialog(&mut state);
assert_eq!(
state.diagram().selected(),
selected,
"the dialog changed the selection"
);
assert!(
!state.diagram().can_undo(),
"opening the dialog recorded a step"
);
assert!(
!state.diagram().can_redo(),
"opening the dialog left a step ahead"
);
assert!(
state.diagram().owner(Offset { x: 2, y: 1 }).is_some(),
"opening the dialog removed the box"
);
}
#[test]
fn escape_closes_the_export_dialog_and_the_event_reaches_nothing_else() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
open_the_export_dialog(&mut state);
assert!(state.export().is_up(), "the export dialog is not open");
let focus = state.chrome().leaf();
let outcome = send(&key(KeyCode::Esc), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(
!state.export().is_up(),
"escape did not close the export dialog"
);
assert_eq!(
state.chrome().leaf(),
focus,
"the closing event moved the keyboard"
);
assert!(
!state.chrome().popup_active(),
"the closing event opened a drop-down"
);
assert_eq!(
state.diagram().selected(),
selected,
"the closing event changed the selection"
);
assert!(
!state.diagram().can_undo(),
"the closing event recorded a step"
);
}
#[test]
fn control_q_does_nothing_while_the_export_dialog_is_up() {
let mut state = a_drawn_state_over(A_BOX);
open_the_export_dialog(&mut state);
let (column, row) = frame::cell_of(&draw(&mut state), "File");
let focus = state.chrome().leaf();
let outcome = send(
&key_with(KeyCode::Char('q'), KeyModifiers::CONTROL),
&mut state,
);
assert!(
matches!(outcome, Control::Continue),
"the way out was answered through the export dialog: {outcome:?}"
);
assert!(state.export().is_up(), "Ctrl+Q closed the export dialog");
let _ = send(&click(column, row), &mut state);
assert_eq!(
state.chrome().leaf(),
focus,
"the click on the bar moved the keyboard"
);
assert!(
!state.chrome().popup_active(),
"the click behind the export dialog opened a drop-down"
);
assert!(
state.export().is_up(),
"the click on the bar closed the export dialog"
);
}
#[test]
fn a_diagram_is_exported_as_text_over_its_footprint_and_overwrites_existing_content() {
let path = std::env::temp_dir().join(format!(
"monospace-export-overwrite-{}.txt",
std::process::id()
));
let path_str = path.to_str().expect("temp path is UTF-8");
std::fs::write(&path, "old content").expect("temp file is writable");
let mut state = a_drawn_state_over(A_BOX);
open_the_export_dialog(&mut state);
state
.export_mut()
.get_mut()
.expect("the export dialog is up")
.set_path(path_str);
let outcome = send(&key(KeyCode::Enter), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(
!state.export().is_up(),
"exporting left the export dialog up"
);
assert_eq!(
state.chrome().leaf(),
Focus::Region,
"exporting moved the keyboard off the region"
);
let read = std::fs::read_to_string(&path).expect("exported file can be read");
assert_eq!(
read, "┌──┐\n│ │\n└──┘\n",
"exported text did not match the 4x3 box footprint"
);
let _ = std::fs::remove_file(path);
}
#[test]
fn an_empty_diagram_exports_an_empty_file() {
let path =
std::env::temp_dir().join(format!("monospace-export-empty-{}.txt", std::process::id()));
let path_str = path.to_str().expect("temp path is UTF-8");
std::fs::write(&path, "old content").expect("temp file is writable");
let mut state = a_drawn_state_over(
r#"{
"next_id": 1,
"shapes": []
}"#,
);
open_the_export_dialog(&mut state);
state
.export_mut()
.get_mut()
.expect("the export dialog is up")
.set_path(path_str);
let outcome = send(&key(KeyCode::Enter), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(
!state.export().is_up(),
"exporting left the export dialog up"
);
let read = std::fs::read_to_string(&path).expect("exported file can be read");
assert_eq!(read, "", "empty diagram did not export an empty file");
let _ = std::fs::remove_file(path);
}
#[test]
fn exporting_leaves_the_current_session_and_history_intact() {
let path = std::env::temp_dir().join(format!(
"monospace-export-intact-{}.txt",
std::process::id()
));
let path_str = path.to_str().expect("temp path is UTF-8");
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
assert!(selected.is_some(), "the box was not selected");
let _ = send(&key(KeyCode::Right), &mut state);
assert!(state.diagram().can_undo(), "movement created no undo step");
open_the_export_dialog(&mut state);
state
.export_mut()
.get_mut()
.expect("the export dialog is up")
.set_path(path_str);
let outcome = send(&key(KeyCode::Enter), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(
!state.export().is_up(),
"exporting left the export dialog up"
);
assert_eq!(
state.diagram().selected(),
selected,
"exporting changed the selection"
);
assert!(
state.diagram().can_undo(),
"exporting dropped the undo stack"
);
assert!(!state.diagram().can_redo(), "exporting modified redo stack");
let _ = std::fs::remove_file(path);
}
#[test]
fn an_empty_path_closes_the_export_dialog_and_opens_the_error_dialog() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
open_the_export_dialog(&mut state);
assert_eq!(
state
.export()
.get()
.expect("the export dialog is up")
.path(),
""
);
let outcome = send(&key(KeyCode::Enter), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(
!state.export().is_up(),
"the failure left the export dialog up"
);
assert!(
state.dialog().is_up(),
"the failure did not open the error dialog"
);
assert_eq!(
state.diagram().selected(),
selected,
"the failure changed the selection"
);
assert!(!state.diagram().can_undo(), "the failure recorded a step");
let area = Rect::new(0, 0, WIDTH, 12);
let mut buffer = Buffer::empty(area);
frame::draw(area, &mut buffer, &mut state, &a_global());
let shown: String = (0..12).map(|line| row_of(&buffer, line)).collect();
assert!(
shown.contains(": a file name is needed"),
"the error dialog does not report the empty path message: {shown:?}"
);
}
#[test]
fn a_path_in_a_missing_directory_closes_the_export_dialog_and_opens_the_error_dialog() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
open_the_export_dialog(&mut state);
let missing_path = "nonexistent_dir_12345/exported.txt";
state
.export_mut()
.get_mut()
.expect("the export dialog is up")
.set_path(missing_path);
let outcome = send(&key(KeyCode::Enter), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(
!state.export().is_up(),
"the failure left the export dialog up"
);
assert!(
state.dialog().is_up(),
"the failure did not open the error dialog"
);
assert_eq!(
state.diagram().selected(),
selected,
"the failure changed the selection"
);
assert!(!state.diagram().can_undo(), "the failure recorded a step");
let area = Rect::new(0, 0, WIDTH, 12);
let mut buffer = Buffer::empty(area);
frame::draw(area, &mut buffer, &mut state, &a_global());
let shown: String = (0..12).map(|line| row_of(&buffer, line)).collect();
assert!(
shown.contains("nonexistent_dir_12345"),
"the error dialog does not name the missing directory: {shown:?}"
);
}
#[test]
fn control_q_does_nothing_while_the_dialog_is_up() {
let mut state = a_drawn_state_over(A_BOX);
open_the_about_dialog(&mut state);
let (column, row) = frame::cell_of(&draw(&mut state), "File");
let focus = state.chrome().leaf();
let outcome = send(
&key_with(KeyCode::Char('q'), KeyModifiers::CONTROL),
&mut state,
);
assert!(
matches!(outcome, Control::Continue),
"the way out was answered through the dialog: {outcome:?}"
);
assert!(state.dialog().is_up(), "Ctrl+Q closed the dialog");
let _ = send(&click(column, row), &mut state);
assert_eq!(
state.chrome().leaf(),
focus,
"the click on the bar moved the keyboard"
);
assert!(
!state.chrome().popup_active(),
"the click behind the dialog opened a drop-down"
);
assert!(
state.dialog().is_up(),
"the click on the bar closed the dialog"
);
}
#[test]
fn the_region_is_byte_identical_after_the_dialog_closes() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let before = draw(&mut state);
open_the_about_dialog(&mut state);
let during = draw(&mut state);
let shown: String = (0..HEIGHT).map(|line| row_of(&during, line)).collect();
assert!(
shown.contains(&format!("About {}", status::NAME)),
"the dialog is not over the region: {shown:?}"
);
let outcome = send(&key(KeyCode::Esc), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
let after = draw(&mut state);
let region = frame::regions(Rect::new(0, 0, WIDTH, HEIGHT)).region;
for line in region.top()..region.bottom() {
for column in region.left()..region.right() {
assert_eq!(
before[(column, line)].symbol(),
after[(column, line)].symbol(),
"({column}, {line}) is not what it was"
);
assert_eq!(
before[(column, line)].style(),
after[(column, line)].style(),
"({column}, {line}) is painted differently"
);
}
}
}
#[test]
fn the_way_out_is_reached_by_the_shortcut_and_by_the_menus_entry() {
let mut state = a_drawn_state();
let outcome = send(
&key_with(KeyCode::Char('q'), KeyModifiers::CONTROL),
&mut state,
);
assert!(matches!(outcome, Control::Quit), "{outcome:?}");
let mut state = a_drawn_state();
let outcome = take_the_entry(&mut state, 'f', "Quit");
assert!(matches!(outcome, Control::Quit), "{outcome:?}");
}
#[test]
fn a_click_on_the_entry_and_its_shortcut_apply_the_same_action() {
let mut state = a_drawn_state();
let by_shortcut = send(
&key_with(KeyCode::Char('q'), KeyModifiers::CONTROL),
&mut state,
);
let mut state = a_drawn_state();
let by_click = take_the_entry(&mut state, 'f', "Quit");
assert_eq!(
by_click, by_shortcut,
"the click and the shortcut are two code paths"
);
}
#[test]
fn the_forced_exit_ends_the_application_whatever_the_state_is() {
let mut state = a_drawn_state();
open_the_drop_down(&mut state, 'f');
let outcome = send(
&key_with(
KeyCode::Char('Q'),
KeyModifiers::CONTROL.union(KeyModifiers::SHIFT),
),
&mut state,
);
assert!(matches!(outcome, Control::Quit), "{outcome:?}");
let mut state = a_drawn_state();
open_the_about_dialog(&mut state);
assert!(state.dialog().is_up(), "the dialog is not open");
let outcome = send(
&key_with(
KeyCode::Char('Q'),
KeyModifiers::CONTROL.union(KeyModifiers::SHIFT),
),
&mut state,
);
assert!(
matches!(outcome, Control::Quit),
"the dialog swallowed the forced exit: {outcome:?}"
);
let mut state = a_drawn_state();
open_the_save_dialog(&mut state);
assert!(state.save().is_up(), "the save dialog is not open");
let outcome = send(
&key_with(
KeyCode::Char('Q'),
KeyModifiers::CONTROL.union(KeyModifiers::SHIFT),
),
&mut state,
);
assert!(
matches!(outcome, Control::Quit),
"the save dialog swallowed the forced exit: {outcome:?}"
);
let mut state = a_drawn_state();
open_the_export_dialog(&mut state);
assert!(state.export().is_up(), "the export dialog is not open");
let outcome = send(
&key_with(
KeyCode::Char('Q'),
KeyModifiers::CONTROL.union(KeyModifiers::SHIFT),
),
&mut state,
);
assert!(
matches!(outcome, Control::Quit),
"the export dialog swallowed the forced exit: {outcome:?}"
);
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let _ = send(&key(KeyCode::Char('t')), &mut state);
assert!(state.text().is_up(), "the text editor is not open");
let outcome = send(
&key_with(
KeyCode::Char('Q'),
KeyModifiers::CONTROL.union(KeyModifiers::SHIFT),
),
&mut state,
);
assert!(
matches!(outcome, Control::Quit),
"the text editor swallowed the forced exit: {outcome:?}"
);
}
#[test]
fn every_shortcut_answered_first_is_answered_with_the_drop_down_open() {
for event in shortcuts::events_answered_first() {
let mut state = a_drawn_state();
open_the_drop_down(&mut state, 'f');
let outcome = send(&event, &mut state);
assert!(
matches!(outcome, Control::Quit),
"{event:?} was swallowed by the chrome: {outcome:?}"
);
}
}
#[test]
fn a_key_with_no_action_changes_nothing() {
let mut state = a_drawn_state();
let outcome = send(&key(KeyCode::Char('z')), &mut state);
assert!(matches!(outcome, Control::Continue), "{outcome:?}");
assert!(
!state.chrome().popup_active(),
"a key with no action changed the state"
);
}
#[test]
fn a_left_click_selects_the_shape_that_owns_the_cell() {
let mut state = a_drawn_state_over(A_BOX);
let owner = state.diagram().owner(Offset { x: 2, y: 1 });
assert!(owner.is_some(), "the box is not in the drawing");
let outcome = send(&click(3, 3), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(
state.diagram().selected(),
owner,
"the click selected another shape"
);
}
#[test]
fn a_click_on_a_cell_nobody_owns_drops_the_selection() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
assert!(
state.diagram().selected().is_some(),
"the box was not selected"
);
let outcome = send(&click(1, 2), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(
state.diagram().selected(),
None,
"the selection survived a blank cell"
);
let outcome = send(&click(1, 2), &mut state);
assert!(
matches!(outcome, Control::Continue),
"there was nothing to drop and the frame was owed: {outcome:?}"
);
}
#[test]
fn the_diagram_region_holds_the_keyboard_at_startup() {
let state = a_state();
assert_eq!(
state.chrome().leaf(),
Focus::Region,
"the bar started with the keyboard"
);
}
#[test]
fn a_press_inside_the_region_moves_the_keyboard_and_selects() {
let mut state = a_drawn_state_over(A_BOX);
state.chrome_mut().move_to_bar();
let _ = draw(&mut state);
let outcome = send(&click(3, 3), &mut state);
assert_eq!(
state.chrome().leaf(),
Focus::Region,
"the press left the keyboard on the bar"
);
assert!(
state.diagram().selected().is_some(),
"the press inside the interior selected nothing"
);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
}
#[test]
fn a_press_on_the_border_moves_the_keyboard_and_selects_nothing() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
assert!(
selected.is_some(),
"the box was not selected, so the test proves nothing"
);
for (column, row, what) in [
(3, 1, "the top border"),
(0, 3, "the left border"),
(WIDTH - 1, 3, "the right border"),
(3, HEIGHT - 3, "the bottom border"),
] {
state.chrome_mut().move_to_bar();
let _ = draw(&mut state);
let outcome = send(&click(column, row), &mut state);
assert_eq!(
state.chrome().leaf(),
Focus::Region,
"{what} did not give the region the keyboard"
);
assert_eq!(
state.diagram().selected(),
selected,
"{what} changed the selection"
);
assert!(matches!(outcome, Control::Changed), "{what}: {outcome:?}");
}
}
#[test]
fn a_click_on_the_status_bar_moves_nothing() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
state.chrome_mut().move_to_bar();
let _ = draw(&mut state);
let outcome = send(&click(3, HEIGHT - 1), &mut state);
assert_eq!(
state.chrome().leaf(),
Focus::Bar,
"the status line took the keyboard"
);
assert_eq!(
state.diagram().selected(),
selected,
"the status line changed the selection"
);
assert!(matches!(outcome, Control::Continue), "{outcome:?}");
}
#[test]
fn a_press_on_a_label_moves_the_keyboard_and_opens_its_drop_down() {
let mut state = a_drawn_state_over(A_BOX);
let buffer = draw(&mut state);
let (column, row) = frame::cell_of(&buffer, "File");
let outcome = send(&click(column, row), &mut state);
assert_eq!(
state.chrome().leaf(),
Focus::Bar,
"the label did not give the bar the keyboard"
);
assert!(
state.chrome().popup_active(),
"the label did not open its drop-down"
);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
}
#[test]
fn a_press_in_the_region_closes_the_drop_down_and_selects() {
let mut state = a_drawn_state_over(A_BOX_AND_WHAT_HANGS_FROM_IT);
open_the_drop_down(&mut state, 'h');
assert!(state.chrome().popup_active(), "the drop-down did not open");
let outcome = send(&click(1, 3), &mut state);
assert_eq!(
state.chrome().leaf(),
Focus::Region,
"the press left the keyboard on the bar; the drop-down is {:?}",
state.chrome().menu().popup.area
);
assert!(
!state.chrome().popup_active(),
"the drop-down survived the keyboard leaving it"
);
assert!(
state.diagram().selected().is_some(),
"the press inside the interior selected nothing"
);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
}
#[test]
fn the_arrows_are_not_the_bars_while_the_region_holds_the_keyboard() {
let mut state = a_drawn_state();
assert_eq!(
state.chrome().leaf(),
Focus::Region,
"the bar started with the keyboard"
);
for arrow in [KeyCode::Left, KeyCode::Right, KeyCode::Up, KeyCode::Down] {
let selected = state.chrome().menu().bar.selected;
let outcome = send(&key(arrow), &mut state);
assert!(
matches!(outcome, Control::Continue),
"{arrow:?}: {outcome:?}"
);
assert!(
!state.chrome().popup_active(),
"{arrow:?} opened a drop-down"
);
assert_eq!(
state.chrome().menu().bar.selected,
selected,
"{arrow:?} moved the bar's label"
);
}
}
#[test]
fn the_right_arrow_moves_the_selected_shape_one_cell() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
assert!(
state.diagram().selected().is_some(),
"the box was not selected"
);
let before = draw(&mut state);
let outcome = send(&key(KeyCode::Right), &mut state);
let after = draw(&mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(state.diagram().can_undo(), "the move recorded no step");
assert_eq!(
state.diagram().owner(Offset { x: 3, y: 1 }),
state.diagram().selected(),
"the drawing does not record the box one cell right"
);
assert_eq!(
before[(3, 3)].symbol(),
"┌",
"the test's frame does not start where it says"
);
assert_eq!(
after[(4, 3)].symbol(),
"┌",
"the frame does not draw the box one cell right"
);
assert_eq!(
after[(3, 3)].symbol(),
" ",
"the frame still draws the box where it stood"
);
}
#[test]
fn each_arrow_moves_along_its_own_axis() {
for (arrow, at) in [
(KeyCode::Right, Offset { x: 3, y: 1 }),
(KeyCode::Left, Offset { x: 1, y: 1 }),
(KeyCode::Up, Offset { x: 2, y: 0 }),
(KeyCode::Down, Offset { x: 2, y: 2 }),
] {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
assert!(
state.diagram().selected().is_some(),
"{arrow:?}: the box was not selected"
);
let outcome = send(&key(arrow), &mut state);
let _ = draw(&mut state);
assert!(
matches!(outcome, Control::Changed),
"{arrow:?}: {outcome:?}"
);
assert_eq!(
state.diagram().owner(at),
state.diagram().selected(),
"{arrow:?} did not move the box along its own axis"
);
assert!(state.diagram().can_undo(), "{arrow:?} recorded no step");
}
}
#[test]
fn two_presses_are_two_steps() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let _ = send(&key(KeyCode::Right), &mut state);
let _ = send(&key(KeyCode::Right), &mut state);
let _ = draw(&mut state);
assert!(
state.diagram().owner(Offset { x: 4, y: 1 }).is_some(),
"the two presses did not move the box twice"
);
assert_eq!(
state.diagram().owner(Offset { x: 3, y: 1 }),
None,
"the second press moved nothing"
);
let _ = send(&undo_key(), &mut state);
let _ = draw(&mut state);
assert!(
state.diagram().owner(Offset { x: 3, y: 1 }).is_some(),
"one undo did not give back one press"
);
let _ = send(&undo_key(), &mut state);
let _ = draw(&mut state);
assert!(
state.diagram().owner(Offset { x: 2, y: 1 }).is_some(),
"the second undo did not give back the first press"
);
assert!(
!state.diagram().can_undo(),
"two presses were more than two steps"
);
}
#[test]
fn a_move_keeps_the_selection() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
assert!(selected.is_some(), "the box was not selected");
let _ = send(&key(KeyCode::Right), &mut state);
let buffer = draw(&mut state);
assert_eq!(
state.diagram().selected(),
selected,
"the move dropped the selection"
);
assert!(
reversed(&buffer, 4, 3),
"the frame does not invert the cells the box now owns"
);
assert!(
!reversed(&buffer, 3, 3),
"the frame still inverts the cell the box left"
);
}
#[test]
fn an_action_resize_resizes_the_selected_shape_and_keeps_selection() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
assert!(selected.is_some(), "the box was not selected");
apply(
&Action::Command(Command::Resize {
id: selected.unwrap(),
by: (1, 0).into(),
}),
&mut state,
);
assert_eq!(state.diagram().selected(), selected);
assert!(state.diagram().can_undo());
let shape = state.diagram().session().diagram().get(selected.unwrap());
let Some(monospace_diagram::Shape::Box { size, .. }) = shape else {
panic!("expected box");
};
assert_eq!(size.width, 5);
}
#[test]
fn shift_arrows_resize_the_selected_box_by_one_cell() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
assert!(selected.is_some(), "the box was not selected");
let shift_right = Event::Key(KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT));
let outcome = send(&shift_right, &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
let shape = state.diagram().session().diagram().get(selected.unwrap());
let Some(monospace_diagram::Shape::Box { size, .. }) = shape else {
panic!("expected box");
};
assert_eq!(size.width, 5);
assert_eq!(size.height, 3);
}
#[test]
fn a_box_at_its_minimum_accepts_the_resize_and_records_no_step() {
let mut state = a_drawn_state_over(A_BOX_AT_ITS_SMALLEST);
let _ = send(&click(3, 3), &mut state);
assert!(
state.diagram().selected().is_some(),
"the box was not selected"
);
let shift_left = Event::Key(KeyEvent::new(KeyCode::Left, KeyModifiers::SHIFT));
let outcome = send(&shift_left, &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(
!state.diagram().can_undo(),
"a resize that changes no size records no step"
);
}
#[test]
fn a_resize_with_nothing_selected_performs_no_command() {
let mut state = a_drawn_state_over(A_BOX);
let shift_right = Event::Key(KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT));
let outcome = send(&shift_right, &mut state);
assert!(matches!(outcome, Control::Continue), "{outcome:?}");
assert!(!state.diagram().can_undo());
}
#[test]
fn a_resize_keeps_the_selection() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
let shift_right = Event::Key(KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT));
let _ = send(&shift_right, &mut state);
assert_eq!(state.diagram().selected(), selected);
}
#[test]
fn two_resizes_are_two_steps() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
let shift_right = Event::Key(KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT));
let _ = send(&shift_right, &mut state);
let _ = send(&shift_right, &mut state);
let shape = state.diagram().session().diagram().get(selected.unwrap());
let Some(monospace_diagram::Shape::Box { size, .. }) = shape else {
panic!("expected box");
};
assert_eq!(size.width, 6);
state.diagram_mut().undo();
let shape = state.diagram().session().diagram().get(selected.unwrap());
let Some(monospace_diagram::Shape::Box { size, .. }) = shape else {
panic!("expected box");
};
assert_eq!(size.width, 5);
}
#[test]
fn shift_arrows_resize_the_selected_horizontal_line_along_its_orientation() {
let mut state = a_drawn_state_over(A_HORIZONTAL_LINE);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
assert!(selected.is_some(), "the line was not selected");
let shift_up = Event::Key(KeyEvent::new(KeyCode::Up, KeyModifiers::SHIFT));
let shift_down = Event::Key(KeyEvent::new(KeyCode::Down, KeyModifiers::SHIFT));
assert!(matches!(send(&shift_up, &mut state), Control::Continue));
assert!(matches!(send(&shift_down, &mut state), Control::Continue));
assert!(!state.diagram().can_undo());
let shift_right = Event::Key(KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT));
let outcome = send(&shift_right, &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
let shape = state.diagram().session().diagram().get(selected.unwrap());
let Some(monospace_diagram::Shape::Line { len, .. }) = shape else {
panic!("expected line");
};
assert_eq!(*len, 7);
assert!(state.diagram().can_undo());
let shift_left = Event::Key(KeyEvent::new(KeyCode::Left, KeyModifiers::SHIFT));
let outcome = send(&shift_left, &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
let shape = state.diagram().session().diagram().get(selected.unwrap());
let Some(monospace_diagram::Shape::Line { len, .. }) = shape else {
panic!("expected line");
};
assert_eq!(*len, 6);
}
#[test]
fn shift_arrows_resize_the_selected_vertical_line_along_its_orientation() {
let mut state = a_drawn_state_over(A_VERTICAL_LINE);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
assert!(selected.is_some(), "the line was not selected");
let shift_left = Event::Key(KeyEvent::new(KeyCode::Left, KeyModifiers::SHIFT));
let shift_right = Event::Key(KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT));
assert!(matches!(send(&shift_left, &mut state), Control::Continue));
assert!(matches!(send(&shift_right, &mut state), Control::Continue));
assert!(!state.diagram().can_undo());
let shift_down = Event::Key(KeyEvent::new(KeyCode::Down, KeyModifiers::SHIFT));
let outcome = send(&shift_down, &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
let shape = state.diagram().session().diagram().get(selected.unwrap());
let Some(monospace_diagram::Shape::Line { len, .. }) = shape else {
panic!("expected line");
};
assert_eq!(*len, 4);
let shift_up = Event::Key(KeyEvent::new(KeyCode::Up, KeyModifiers::SHIFT));
let outcome = send(&shift_up, &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
let shape = state.diagram().session().diagram().get(selected.unwrap());
let Some(monospace_diagram::Shape::Line { len, .. }) = shape else {
panic!("expected line");
};
assert_eq!(*len, 3);
}
#[test]
fn a_line_at_its_minimum_accepts_the_resize_and_records_no_step() {
let mut state = a_drawn_state_over(A_LINE_AT_ITS_MINIMUM);
let _ = send(&click(3, 3), &mut state);
assert!(
state.diagram().selected().is_some(),
"the line was not selected"
);
let shift_left = Event::Key(KeyEvent::new(KeyCode::Left, KeyModifiers::SHIFT));
let outcome = send(&shift_left, &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(
!state.diagram().can_undo(),
"a resize that changes no length records no step"
);
}
#[test]
fn a_zero_length_line_is_not_shrunk_and_grows_to_one_in_the_application() {
let mut state = a_drawn_state_over(A_ZERO_LENGTH_LINE);
state
.diagram_mut()
.select(Some(ShapeId::new(NonZeroU16::new(1).unwrap())));
let shift_left = Event::Key(KeyEvent::new(KeyCode::Left, KeyModifiers::SHIFT));
let outcome = send(&shift_left, &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
let shape = state
.diagram()
.session()
.diagram()
.get(ShapeId::new(NonZeroU16::new(1).unwrap()));
let Some(monospace_diagram::Shape::Line { len, .. }) = shape else {
panic!("expected line");
};
assert_eq!(*len, 0);
assert!(!state.diagram().can_undo());
let shift_right = Event::Key(KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT));
let outcome = send(&shift_right, &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
let shape = state
.diagram()
.session()
.diagram()
.get(ShapeId::new(NonZeroU16::new(1).unwrap()));
let Some(monospace_diagram::Shape::Line { len, .. }) = shape else {
panic!("expected line");
};
assert_eq!(*len, 1);
assert!(state.diagram().can_undo());
}
#[test]
fn two_line_resizes_are_two_steps() {
let mut state = a_drawn_state_over(A_HORIZONTAL_LINE);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
let shift_right = Event::Key(KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT));
let _ = send(&shift_right, &mut state);
let _ = send(&shift_right, &mut state);
let shape = state.diagram().session().diagram().get(selected.unwrap());
let Some(monospace_diagram::Shape::Line { len, .. }) = shape else {
panic!("expected line");
};
assert_eq!(*len, 8);
state.diagram_mut().undo();
let shape = state.diagram().session().diagram().get(selected.unwrap());
let Some(monospace_diagram::Shape::Line { len, .. }) = shape else {
panic!("expected line");
};
assert_eq!(*len, 7);
state.diagram_mut().undo();
let shape = state.diagram().session().diagram().get(selected.unwrap());
let Some(monospace_diagram::Shape::Line { len, .. }) = shape else {
panic!("expected line");
};
assert_eq!(*len, 6);
state.diagram_mut().redo();
let shape = state.diagram().session().diagram().get(selected.unwrap());
let Some(monospace_diagram::Shape::Line { len, .. }) = shape else {
panic!("expected line");
};
assert_eq!(*len, 7);
}
#[test]
fn a_line_resize_keeps_the_selection() {
let mut state = a_drawn_state_over(A_HORIZONTAL_LINE);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
let shift_right = Event::Key(KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT));
let _ = send(&shift_right, &mut state);
assert_eq!(state.diagram().selected(), selected);
}
#[test]
fn a_plain_arrow_still_moves() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
let right = Event::Key(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE));
let _ = send(&right, &mut state);
let shape = state.diagram().session().diagram().get(selected.unwrap());
let Some(monospace_diagram::Shape::Box { at, size, .. }) = shape else {
panic!("expected box");
};
assert_eq!(at.x, 3);
assert_eq!(size.width, 4);
}
#[test]
fn no_other_modifier_resizes() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let ctrl_shift_right = Event::Key(KeyEvent::new(
KeyCode::Right,
KeyModifiers::CONTROL | KeyModifiers::SHIFT,
));
let outcome = send(&ctrl_shift_right, &mut state);
assert!(matches!(outcome, Control::Continue), "{outcome:?}");
assert!(!state.diagram().can_undo());
}
#[test]
fn shift_arrow_with_the_bar_focused_resizes_nothing() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let _ = send(&key(KeyCode::F(10)), &mut state);
let shift_right = Event::Key(KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT));
let _ = send(&shift_right, &mut state);
assert!(!state.diagram().can_undo());
}
#[test]
fn a_resize_carries_the_connector_hanging_from_the_right_side() {
let text = r#"{
"next_id": 3,
"shapes": [
{ "kind": "box", "id": 1, "at": { "x": 2, "y": 1 }, "size": { "width": 4, "height": 3 },
"stroke": "light" },
{ "kind": "connector", "id": 2,
"from": { "at": { "kind": "reference", "shape": 1, "anchor": "right",
"offset": { "dx": 2, "dy": 0 } },
"terminal": { "kind": "arm" } },
"to": { "at": { "kind": "point", "x": 15, "y": 2 },
"leaving": "left", "terminal": { "kind": "arm" } },
"stroke": "light" }
]
}"#;
let mut state = a_drawn_state_over(text);
let _ = send(&click(3, 3), &mut state);
let shift_right = Event::Key(KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT));
let _ = send(&shift_right, &mut state);
let shape = state
.diagram()
.session()
.diagram()
.get(state.diagram().selected().expect("shape was selected"));
let Some(monospace_diagram::Shape::Box { size, .. }) = shape else {
panic!("expected box");
};
assert_eq!(size.width, 5);
}
#[test]
fn a_move_with_nothing_selected_moves_nothing() {
let mut state = a_drawn_state_over(A_BOX);
let outcome = send(&key(KeyCode::Right), &mut state);
assert!(matches!(outcome, Control::Continue), "{outcome:?}");
assert!(
!state.diagram().can_undo(),
"a move with nothing selected recorded a step"
);
assert!(
state.diagram().owner(Offset { x: 2, y: 1 }).is_some(),
"the box moved with nothing selected"
);
}
#[test]
fn an_arrow_while_the_dialog_is_up_moves_nothing() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
assert!(
state.diagram().selected().is_some(),
"the box was not selected"
);
open_the_about_dialog(&mut state);
let _ = send(&key(KeyCode::Right), &mut state);
assert!(
state.dialog().is_up(),
"the arrow closed the dialog it should not have reached"
);
assert!(
!state.diagram().can_undo(),
"a key the dialog swallowed recorded a step"
);
assert!(
state.diagram().owner(Offset { x: 2, y: 1 }).is_some(),
"the box moved behind the dialog"
);
}
#[test]
fn a_move_leaves_the_keyboard_on_the_region() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
assert!(
state.diagram().selected().is_some(),
"the box was not selected"
);
state.chrome_mut().menu_mut().set_popup_active(true);
let outcome = send(&key(KeyCode::Right), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(
state.chrome().leaf(),
Focus::Region,
"the move left the keyboard on the bar"
);
assert!(
!state.chrome().popup_active(),
"the move left a drop-down open"
);
assert!(state.diagram().can_undo(), "the move recorded no step");
}
#[test]
fn a_frame_after_a_move_draws_the_shape_one_cell_further_on() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let before = draw(&mut state);
let _ = send(&key(KeyCode::Right), &mut state);
let after = draw(&mut state);
assert_eq!(
before[(3, 3)].symbol(),
after[(4, 3)].symbol(),
"the frame does not draw the figure one cell further on"
);
assert_eq!(
after[(3, 3)].symbol(),
" ",
"the frame still draws the figure where it stood"
);
}
#[test]
fn a_frame_after_a_move_out_of_the_window_draws_nothing() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
for _ in 0..7 {
let _ = send(&key(KeyCode::Left), &mut state);
}
let _ = draw(&mut state);
assert!(state.diagram().can_undo(), "the presses recorded no step");
let catalog = monospace_glyph_sets::all();
for y in 0..6 {
for x in 0..38 {
assert!(
state.diagram().glyph(Offset { x, y }, &catalog).is_none(),
"({x}, {y}) still draws the figure that left the window"
);
}
}
}
#[test]
fn alt_and_a_labels_character_opens_that_labels_drop_down() {
for (label, text) in [('f', "Quit"), ('e', "Undo"), ('h', "About")] {
let mut state = a_drawn_state();
assert_eq!(
state.chrome().leaf(),
Focus::Region,
"the test starts on the bar"
);
open_the_drop_down(&mut state, label);
let frame = draw(&mut state);
let shown: String = (0..HEIGHT).map(|line| row_of(&frame, line)).collect();
assert!(
shown.contains(text),
"Alt+{label} opened another drop-down: {shown:?}"
);
}
}
#[test]
fn alt_with_a_character_that_names_no_label_changes_nothing() {
let mut state = a_drawn_state();
let outcome = send(&key_with(KeyCode::Char('z'), KeyModifiers::ALT), &mut state);
assert!(matches!(outcome, Control::Continue), "{outcome:?}");
assert_eq!(
state.chrome().leaf(),
Focus::Region,
"a combination that names no label moved the keyboard"
);
assert!(!state.chrome().popup_active(), "a drop-down opened");
}
#[test]
fn a_second_escape_gives_the_keyboard_back_to_the_region() {
let mut state = a_drawn_state();
open_the_drop_down(&mut state, 'f');
let outcome = send(&key(KeyCode::Esc), &mut state);
let frame = draw(&mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(
!state.chrome().popup_active(),
"escape left the drop-down open"
);
assert_eq!(
state.chrome().leaf(),
Focus::Bar,
"the first escape left the bar"
);
let (column, row) = frame::cell_of(&frame, "File");
assert!(
reversed(&frame, column, row),
"the label stopped looking focused with the bar still holding the keyboard"
);
let outcome = send(&key(KeyCode::Esc), &mut state);
let frame = draw(&mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(
state.chrome().leaf(),
Focus::Region,
"the second escape did not give the keyboard back"
);
assert!(
!reversed(&frame, column, row),
"a label is reversed with the region holding the keyboard"
);
}
#[test]
fn escape_in_the_region_changes_nothing() {
let mut state = a_drawn_state();
assert_eq!(
state.chrome().leaf(),
Focus::Region,
"the region does not start with the keyboard"
);
let outcome = send(&key(KeyCode::Esc), &mut state);
assert!(matches!(outcome, Control::Continue), "{outcome:?}");
assert_eq!(
state.chrome().leaf(),
Focus::Region,
"escape moved the keyboard"
);
assert!(!state.chrome().popup_active(), "escape opened a drop-down");
}
#[test]
fn f10_gives_the_bar_the_keyboard_without_opening_a_drop_down() {
let mut state = a_drawn_state();
assert_eq!(
state.chrome().leaf(),
Focus::Region,
"the test starts on the bar"
);
let outcome = send(&key(KeyCode::F(10)), &mut state);
let frame = draw(&mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(
state.chrome().leaf(),
Focus::Bar,
"F10 did not give the bar the keyboard"
);
assert!(!state.chrome().popup_active(), "F10 opened a drop-down");
let (column, row) = frame::cell_of(&frame, "File");
assert!(
reversed(&frame, column, row),
"the bar does not look focused"
);
}
#[test]
fn a_click_the_drop_down_takes_never_reaches_the_diagram() {
let mut state = a_drawn_state_over(A_BOX);
let outcome = take_the_entry(&mut state, 'f', "Quit");
assert!(matches!(outcome, Control::Quit), "{outcome:?}");
assert_eq!(
state.diagram().selected(),
None,
"the click the drop-down took reached the diagram"
);
}
#[test]
fn a_click_on_the_drop_down_border_never_reaches_the_diagram() {
let mut state = a_drawn_state_over(A_BOX);
open_the_drop_down(&mut state, 'e');
let popup = state.chrome().menu().popup.area;
let mut border = None;
for row in popup.top()..popup.bottom() {
for column in popup.left()..popup.right() {
let at = Position::new(column, row);
if state.chrome().menu().popup.item_at((column, row)).is_some() {
continue;
}
let Some(offset) = state.diagram().offset_of(at) else {
continue;
};
if state.diagram().owner(offset).is_some() {
border = Some(at);
break;
}
}
if border.is_some() {
break;
}
}
let border = border.unwrap_or_else(|| {
panic!(
"the Edit drop-down {popup:?} has no border cell over the box, so the test proves nothing"
)
});
assert_eq!(
state.diagram().selected(),
None,
"the test starts with a selection"
);
let outcome = send(&click(border.x, border.y), &mut state);
assert_eq!(
state.diagram().selected(),
None,
"the click on the drop-down's border reached the drawing behind it at {border:?}"
);
assert_eq!(
state.chrome().leaf(),
Focus::Bar,
"the click on the drop-down's border moved the keyboard"
);
assert!(
state.chrome().popup_active(),
"the click on the drop-down's border closed the drop-down"
);
assert!(matches!(outcome, Control::Continue), "{outcome:?}");
}
#[test]
fn the_delete_key_removes_the_selected_shape() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
assert!(
state.diagram().selected().is_some(),
"the box was not selected"
);
let outcome = send(&key(KeyCode::Delete), &mut state);
let buffer = draw(&mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(
state.diagram().owner(Offset { x: 2, y: 1 }).is_none(),
"the drawing still records the removed box"
);
assert_eq!(
buffer[(3, 3)].symbol(),
" ",
"the frame still draws the removed box"
);
}
#[test]
fn the_delete_key_drops_the_selection() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
assert!(
reversed(&draw(&mut state), 3, 3),
"the click selected nothing, so the test proves nothing"
);
let _ = send(&key(KeyCode::Delete), &mut state);
let buffer = draw(&mut state);
assert_eq!(
state.diagram().selected(),
None,
"the identity survived the removal"
);
assert!(
!reversed(&buffer, 3, 3),
"the frame after the removal inverts a cell"
);
}
#[test]
fn the_delete_key_with_nothing_selected_is_ignored() {
let mut state = a_drawn_state_over(A_BOX);
let outcome = send(&key(KeyCode::Delete), &mut state);
assert!(matches!(outcome, Control::Continue), "{outcome:?}");
assert!(
!state.diagram().can_undo(),
"a press with nothing selected recorded a step"
);
assert_eq!(
state.diagram().selected(),
None,
"a press with nothing selected left a selection"
);
assert!(
state.diagram().owner(Offset { x: 2, y: 1 }).is_some(),
"the box was removed without a selection"
);
}
#[test]
fn the_drop_down_keeps_the_delete_key() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
open_the_drop_down(&mut state, 'f');
let outcome = send(&key(KeyCode::Delete), &mut state);
assert!(matches!(outcome, Control::Continue), "{outcome:?}");
assert!(
state.diagram().selected().is_some(),
"the figure was deleted"
);
assert_eq!(state.chrome().leaf(), Focus::Bar, "focus left the bar");
assert!(state.chrome().popup_active(), "the drop-down was closed");
}
#[test]
fn the_delete_key_on_the_bar_does_nothing() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let _ = send(&key(KeyCode::F(10)), &mut state);
assert_eq!(state.chrome().leaf(), Focus::Bar);
let outcome = send(&key(KeyCode::Delete), &mut state);
assert!(matches!(outcome, Control::Continue), "{outcome:?}");
assert!(
state.diagram().selected().is_some(),
"the figure was deleted"
);
}
#[test]
fn click_outside_cancels_sub_mode_and_clears_selection() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let _ = send(&key(KeyCode::Char('s')), &mut state);
assert_eq!(
state.diagram().sub_mode(),
Some(crate::diagram_state::SubMode::StrokeSelect)
);
let _ = send(&click(1, 2), &mut state);
assert_eq!(state.diagram().sub_mode(), None);
assert_eq!(state.diagram().selected(), None);
}
#[test]
fn clicking_another_figure_cancels_sub_mode_and_changes_selection() {
let mut state = a_drawn_state_over(A_BOX_AND_WHAT_HANGS_FROM_IT);
let _ = send(&click(3, 3), &mut state);
let first = state.diagram().selected();
assert!(first.is_some());
let _ = send(&key(KeyCode::Char('s')), &mut state);
assert_eq!(
state.diagram().sub_mode(),
Some(crate::diagram_state::SubMode::StrokeSelect)
);
let _ = send(&click(18, 3), &mut state);
let second = state.diagram().selected();
assert!(second.is_some());
assert_ne!(first, second);
assert_eq!(state.diagram().sub_mode(), None);
}
#[test]
fn clicking_the_same_figure_in_sub_mode_preserves_sub_mode() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let _ = send(&key(KeyCode::Char('s')), &mut state);
assert_eq!(
state.diagram().sub_mode(),
Some(crate::diagram_state::SubMode::StrokeSelect)
);
let _ = send(&click(3, 3), &mut state);
assert_eq!(
state.diagram().sub_mode(),
Some(crate::diagram_state::SubMode::StrokeSelect)
);
}
#[test]
fn moving_focus_to_bar_and_returning_preserves_sub_mode() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let _ = send(&key(KeyCode::Char('s')), &mut state);
assert_eq!(
state.diagram().sub_mode(),
Some(crate::diagram_state::SubMode::StrokeSelect)
);
let _ = send(&key(KeyCode::F(10)), &mut state);
assert_eq!(state.chrome().leaf(), Focus::Bar);
assert_eq!(
state.diagram().sub_mode(),
Some(crate::diagram_state::SubMode::StrokeSelect)
);
let _ = send(&key(KeyCode::Esc), &mut state);
assert_eq!(state.chrome().leaf(), Focus::Region);
assert_eq!(
state.diagram().sub_mode(),
Some(crate::diagram_state::SubMode::StrokeSelect)
);
assert!(state.diagram().selected().is_some());
}
#[test]
fn opening_dialog_and_closing_preserves_sub_mode() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let _ = send(&key(KeyCode::Char('s')), &mut state);
assert_eq!(
state.diagram().sub_mode(),
Some(crate::diagram_state::SubMode::StrokeSelect)
);
let _ = send(
&key_with(KeyCode::Char('s'), KeyModifiers::CONTROL),
&mut state,
);
assert!(state.save().is_up());
assert_eq!(
state.diagram().sub_mode(),
Some(crate::diagram_state::SubMode::StrokeSelect)
);
let _ = send(&key(KeyCode::Esc), &mut state);
assert!(!state.save().is_up());
assert_eq!(
state.diagram().sub_mode(),
Some(crate::diagram_state::SubMode::StrokeSelect)
);
assert!(state.diagram().selected().is_some());
}
fn stroke_of(shape: &monospace_diagram::Shape) -> monospace_diagram::Stroke {
match shape {
monospace_diagram::Shape::Box { stroke, .. }
| monospace_diagram::Shape::Line { stroke, .. }
| monospace_diagram::Shape::Connector { stroke, .. } => stroke.clone(),
}
}
#[test]
fn setting_identical_stroke_leaves_diagram_untouched_and_records_no_step() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
assert!(selected.is_some(), "the box was not selected");
let _ = send(&key(KeyCode::Char('s')), &mut state);
let outcome = send(&key(KeyCode::Char('1')), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(
state.diagram().sub_mode(),
None,
"the sub-mode survived the stroke it was asked for"
);
assert_eq!(
stroke_of(
state
.diagram()
.selected_shape()
.expect("the box is still there")
),
monospace_diagram::Stroke::from("light"),
"the stroke changed"
);
assert!(
!state.diagram().can_undo(),
"an unchanged stroke recorded a step"
);
assert_eq!(
state.diagram().selected(),
selected,
"the identity went with the stroke"
);
}
#[test]
fn setting_different_stroke_records_one_step_in_session_history() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
assert!(selected.is_some(), "the box was not selected");
let _ = send(&key(KeyCode::Char('s')), &mut state);
let outcome = send(&key(KeyCode::Char('2')), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(
state.diagram().sub_mode(),
None,
"the sub-mode survived the stroke it was asked for"
);
assert_eq!(
stroke_of(
state
.diagram()
.selected_shape()
.expect("the box is still there")
),
monospace_diagram::Stroke::from("double"),
"the stroke did not change"
);
assert!(state.diagram().can_undo(), "no step was recorded");
assert_eq!(
state.diagram().selected(),
selected,
"the identity went with the stroke"
);
let _ = send(&undo_key(), &mut state);
assert_eq!(
state.diagram().selected(),
None,
"the undo kept the selection"
);
let restored_shape = state
.diagram()
.session()
.diagram()
.get(selected.unwrap())
.expect("the box is still in the diagram");
assert_eq!(
stroke_of(restored_shape),
monospace_diagram::Stroke::from("light"),
"one undo did not give the old stroke back"
);
assert!(
!state.diagram().can_undo(),
"the stroke recorded more than one step"
);
}
#[test]
fn undo_and_redo_in_stroke_select_walk_history() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let selected = state.diagram().selected();
assert!(selected.is_some(), "the box was not selected");
let _ = send(&key(KeyCode::Char('s')), &mut state);
let _ = send(&key(KeyCode::Char('2')), &mut state);
let _ = send(&key(KeyCode::Char('s')), &mut state);
assert_eq!(
state.diagram().sub_mode(),
Some(crate::diagram_state::SubMode::StrokeSelect),
"the sub-mode was not entered again"
);
let outcome = send(&undo_key(), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
let restored_shape = state
.diagram()
.session()
.diagram()
.get(selected.unwrap())
.expect("the box is still in the diagram");
assert_eq!(
stroke_of(restored_shape),
monospace_diagram::Stroke::from("light"),
"the undo did not give the old stroke back"
);
assert_eq!(
state.diagram().sub_mode(),
None,
"the undo kept the sub-mode"
);
assert_eq!(
state.diagram().selected(),
None,
"the undo kept the selection"
);
let outcome = send(&redo_key(), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
let redone_shape = state
.diagram()
.session()
.diagram()
.get(selected.unwrap())
.expect("the box is still in the diagram");
assert_eq!(
stroke_of(redone_shape),
monospace_diagram::Stroke::from("double"),
"the redo did not give the new stroke back"
);
assert_eq!(
state.diagram().sub_mode(),
None,
"the redo restored a sub-mode"
);
assert_eq!(
state.diagram().selected(),
None,
"the redo restored a selection"
);
}
const TWO_OVERLAPPING_BOXES: &str = r#"{
"next_id": 3,
"shapes": [
{ "kind": "box", "id": 1, "at": { "x": 0, "y": 1 },
"size": { "width": 4, "height": 3 }, "stroke": "light", "fill": "░" },
{ "kind": "box", "id": 2, "at": { "x": 2, "y": 2 },
"size": { "width": 4, "height": 3 }, "stroke": "light", "fill": "▓" }
]
}"#;
#[test]
fn z_opens_order_mode_and_escape_leaves_it() {
let mut state = a_drawn_state_over(TWO_OVERLAPPING_BOXES);
let _ = send(&click(1, 3), &mut state);
let id = state.diagram().selected();
assert!(id.is_some(), "box #1 was not selected");
let outcome = send(&key(KeyCode::Char('z')), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(
state.diagram().sub_mode(),
Some(crate::diagram_state::SubMode::OrderMode),
"z did not enter order mode"
);
let buffer = draw(&mut state);
assert!(
row_of(&buffer, HEIGHT - 2).starts_with("LAYER ORDER for Box #1"),
"status row did not report layer order: {}",
row_of(&buffer, HEIGHT - 2)
);
assert!(
row_of(&buffer, HEIGHT - 1).contains("ORDER: [Home] Front"),
"command row did not offer order commands: {}",
row_of(&buffer, HEIGHT - 1)
);
let outcome = send(&key(KeyCode::Esc), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(
state.diagram().sub_mode(),
None,
"Esc did not leave order mode"
);
assert_eq!(state.diagram().selected(), id, "Esc dropped the selection");
}
#[test]
fn page_up_moves_the_selected_figure_forward_and_records_one_step() {
let mut state = a_drawn_state_over(TWO_OVERLAPPING_BOXES);
let _ = send(&click(1, 3), &mut state);
let id1 = state.diagram().selected().expect("box #1 selected");
assert_eq!(
state.diagram().session().diagram().place_of(id1),
Some(0),
"box #1 starts at the back"
);
let _ = send(&key(KeyCode::Char('z')), &mut state);
let outcome = send(&key(KeyCode::PageUp), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(
state.diagram().session().diagram().place_of(id1),
Some(1),
"PageUp did not move box #1 forward"
);
assert!(
state.diagram().session().can_undo(),
"moving forward recorded no step"
);
assert_eq!(
state.diagram().sub_mode(),
Some(crate::diagram_state::SubMode::OrderMode),
"PageUp left order mode"
);
}
#[test]
fn page_down_moves_the_selected_figure_backward() {
let mut state = a_drawn_state_over(TWO_OVERLAPPING_BOXES);
let _ = send(&click(5, 4), &mut state);
let id2 = state.diagram().selected().expect("box #2 selected");
assert_eq!(
state.diagram().session().diagram().place_of(id2),
Some(1),
"box #2 starts in front"
);
let _ = send(&key(KeyCode::Char('z')), &mut state);
let outcome = send(&key(KeyCode::PageDown), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(
state.diagram().session().diagram().place_of(id2),
Some(0),
"PageDown did not move box #2 backward"
);
assert!(
state.diagram().session().can_undo(),
"moving backward recorded no step"
);
assert_eq!(
state.diagram().sub_mode(),
Some(crate::diagram_state::SubMode::OrderMode),
"PageDown left order mode"
);
}
#[test]
fn home_moves_the_selected_figure_to_the_front_in_one_step() {
let mut state = a_drawn_state_over(TWO_OVERLAPPING_BOXES);
let _ = send(&click(1, 3), &mut state);
let id1 = state.diagram().selected().expect("box #1 selected");
assert_eq!(state.diagram().session().diagram().place_of(id1), Some(0));
let _ = send(&key(KeyCode::Char('z')), &mut state);
let outcome = send(&key(KeyCode::Home), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(
state.diagram().session().diagram().place_of(id1),
Some(1),
"Home did not move box #1 to the front"
);
assert!(state.diagram().session().can_undo());
let undo_outcome = send(&undo_key(), &mut state);
assert!(matches!(undo_outcome, Control::Changed), "{undo_outcome:?}");
assert_eq!(
state.diagram().session().diagram().place_of(id1),
Some(0),
"undo did not restore box #1 position"
);
assert!(
!state.diagram().session().can_undo(),
"more than one step recorded"
);
assert_eq!(state.diagram().selected(), None);
assert_eq!(state.diagram().sub_mode(), None);
}
#[test]
fn end_moves_the_selected_figure_to_the_back_in_one_step() {
let mut state = a_drawn_state_over(TWO_OVERLAPPING_BOXES);
let _ = send(&click(5, 4), &mut state);
let id2 = state.diagram().selected().expect("box #2 selected");
assert_eq!(state.diagram().session().diagram().place_of(id2), Some(1));
let _ = send(&key(KeyCode::Char('z')), &mut state);
let outcome = send(&key(KeyCode::End), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(
state.diagram().session().diagram().place_of(id2),
Some(0),
"End did not move box #2 to the back"
);
assert!(state.diagram().session().can_undo());
let undo_outcome = send(&undo_key(), &mut state);
assert!(matches!(undo_outcome, Control::Changed), "{undo_outcome:?}");
assert_eq!(
state.diagram().session().diagram().place_of(id2),
Some(1),
"undo did not restore box #2 position"
);
assert!(
!state.diagram().session().can_undo(),
"more than one step recorded"
);
}
#[test]
fn a_move_at_the_limit_records_no_step() {
let mut state = a_drawn_state_over(TWO_OVERLAPPING_BOXES);
let _ = send(&click(1, 3), &mut state);
let _ = send(&key(KeyCode::Char('z')), &mut state);
let outcome = send(&key(KeyCode::PageDown), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(
!state.diagram().session().can_undo(),
"PageDown at back recorded a step"
);
assert_eq!(
state.diagram().sub_mode(),
Some(crate::diagram_state::SubMode::OrderMode),
"PageDown at back left order mode"
);
let mut state = a_drawn_state_over(TWO_OVERLAPPING_BOXES);
let _ = send(&click(1, 3), &mut state);
let _ = send(&key(KeyCode::Char('z')), &mut state);
let outcome = send(&key(KeyCode::End), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(
!state.diagram().session().can_undo(),
"End at back recorded a step"
);
assert_eq!(
state.diagram().sub_mode(),
Some(crate::diagram_state::SubMode::OrderMode),
"End at back left order mode"
);
let mut state = a_drawn_state_over(TWO_OVERLAPPING_BOXES);
let _ = send(&click(5, 4), &mut state);
let _ = send(&key(KeyCode::Char('z')), &mut state);
let outcome = send(&key(KeyCode::PageUp), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(
!state.diagram().session().can_undo(),
"PageUp at front recorded a step"
);
assert_eq!(
state.diagram().sub_mode(),
Some(crate::diagram_state::SubMode::OrderMode),
"PageUp at front left order mode"
);
let mut state = a_drawn_state_over(TWO_OVERLAPPING_BOXES);
let _ = send(&click(5, 4), &mut state);
let _ = send(&key(KeyCode::Char('z')), &mut state);
let outcome = send(&key(KeyCode::Home), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(
!state.diagram().session().can_undo(),
"Home at front recorded a step"
);
assert_eq!(
state.diagram().sub_mode(),
Some(crate::diagram_state::SubMode::OrderMode),
"Home at front left order mode"
);
}
#[test]
fn undo_and_redo_leave_order_mode() {
let mut state = a_drawn_state_over(TWO_OVERLAPPING_BOXES);
let _ = send(&click(1, 3), &mut state);
let id1 = state.diagram().selected().expect("box #1 selected");
let _ = send(&key(KeyCode::Char('z')), &mut state);
let _ = send(&key(KeyCode::Home), &mut state);
assert_eq!(state.diagram().session().diagram().place_of(id1), Some(1));
let undo_outcome = send(&undo_key(), &mut state);
assert!(matches!(undo_outcome, Control::Changed), "{undo_outcome:?}");
assert_eq!(state.diagram().selected(), None);
assert_eq!(state.diagram().sub_mode(), None);
assert_eq!(
state.diagram().session().diagram().place_of(id1),
Some(0),
"undo did not restore original place"
);
let redo_outcome = send(&redo_key(), &mut state);
assert!(matches!(redo_outcome, Control::Changed), "{redo_outcome:?}");
assert_eq!(state.diagram().selected(), None);
assert_eq!(state.diagram().sub_mode(), None);
assert_eq!(
state.diagram().session().diagram().place_of(id1),
Some(1),
"redo did not restore forward place"
);
}
#[test]
fn clicking_another_figure_cancels_order_mode() {
let mut state = a_drawn_state_over(TWO_OVERLAPPING_BOXES);
let _ = send(&click(1, 3), &mut state);
let id1 = state.diagram().selected();
assert!(id1.is_some());
let _ = send(&key(KeyCode::Char('z')), &mut state);
assert_eq!(
state.diagram().sub_mode(),
Some(crate::diagram_state::SubMode::OrderMode)
);
let outcome = send(&click(5, 4), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
let id2 = state.diagram().selected();
assert!(id2.is_some());
assert_ne!(id1, id2);
assert_eq!(state.diagram().sub_mode(), None);
}
#[test]
fn clicking_outside_cancels_order_mode_and_clears_the_selection() {
let mut state = a_drawn_state_over(TWO_OVERLAPPING_BOXES);
let _ = send(&click(1, 3), &mut state);
assert!(state.diagram().selected().is_some());
let _ = send(&key(KeyCode::Char('z')), &mut state);
assert_eq!(
state.diagram().sub_mode(),
Some(crate::diagram_state::SubMode::OrderMode)
);
let outcome = send(&click(20, 2), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(state.diagram().sub_mode(), None);
assert_eq!(state.diagram().selected(), None);
}
#[test]
fn escape_in_the_region_drops_the_selection() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
assert!(
state.diagram().selected().is_some(),
"the box was not selected"
);
let outcome = send(&key(KeyCode::Esc), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(
state.diagram().selected(),
None,
"Escape did not drop the selection"
);
let second = send(&key(KeyCode::Esc), &mut state);
assert!(matches!(second, Control::Continue), "{second:?}");
}
#[test]
fn t_opens_the_text_editor_on_a_box() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
assert!(
state.diagram().selected().is_some(),
"the box was not selected"
);
let outcome = send(&key(KeyCode::Char('t')), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(state.text().is_up(), "t opened no editor");
assert!(
!state.diagram().can_undo(),
"opening the editor recorded a step"
);
}
#[test]
fn t_is_ignored_off_a_box() {
let mut line = a_drawn_state_over(A_HORIZONTAL_LINE);
let _ = send(&click(3, 3), &mut line);
assert!(
line.diagram().selected().is_some(),
"the line was not selected"
);
let outcome = send(&key(KeyCode::Char('t')), &mut line);
assert!(matches!(outcome, Control::Continue), "line: {outcome:?}");
assert!(!line.text().is_up(), "line: t opened an editor");
let mut connector = a_drawn_state_over(A_CONNECTOR);
connector
.diagram_mut()
.select(Some(ShapeId::new(NonZeroU16::new(1).unwrap())));
let outcome = send(&key(KeyCode::Char('t')), &mut connector);
assert!(
matches!(outcome, Control::Continue),
"connector: {outcome:?}"
);
assert!(!connector.text().is_up(), "connector: t opened an editor");
let mut nothing = a_drawn_state_over(A_BOX);
let outcome = send(&key(KeyCode::Char('t')), &mut nothing);
assert!(
matches!(outcome, Control::Continue),
"nothing selected: {outcome:?}"
);
assert!(
!nothing.text().is_up(),
"nothing selected: t opened an editor"
);
let mut sub_mode = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut sub_mode);
let _ = send(&key(KeyCode::Char('s')), &mut sub_mode);
assert_eq!(
sub_mode.diagram().sub_mode(),
Some(crate::diagram_state::SubMode::StrokeSelect)
);
let outcome = send(&key(KeyCode::Char('t')), &mut sub_mode);
assert!(
matches!(outcome, Control::Continue),
"sub-mode: {outcome:?}"
);
assert!(!sub_mode.text().is_up(), "sub-mode: t opened an editor");
}
#[test]
fn pressing_i_on_selected_connector_inverts_terminals() {
let fixture = r#"{
"next_id": 2,
"shapes": [ { "kind": "connector", "id": 1,
"from": { "at": { "kind": "point", "x": 2, "y": 1 },
"leaving": "right", "terminal": { "kind": "arm" } },
"to": { "at": { "kind": "point", "x": 10, "y": 1 },
"leaving": "left", "terminal": { "kind": "symbol", "style": "solid" } },
"stroke": "light" } ]
}"#;
let mut state = a_drawn_state_over(fixture);
let id = ShapeId::new(NonZeroU16::new(1).unwrap());
state.diagram_mut().select(Some(id));
let i_key = Event::Key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));
let outcome = send(&i_key, &mut state);
assert!(matches!(outcome, Control::Changed));
let shape = state.diagram().session().diagram().get(id);
let Some(monospace_diagram::Shape::Connector { from, to, .. }) = shape else {
panic!("expected connector");
};
assert_eq!(
from.terminal,
monospace_core::Terminal::Symbol {
style: monospace_core::SymbolStyleName::from("solid")
}
);
assert_eq!(to.terminal, monospace_core::Terminal::Arm);
assert!(state.diagram().session().can_undo());
let undo_key = Event::Key(KeyEvent::new(KeyCode::Char('z'), KeyModifiers::CONTROL));
let undo_outcome = send(&undo_key, &mut state);
assert!(matches!(undo_outcome, Control::Changed));
let shape_restored = state.diagram().session().diagram().get(id);
let Some(monospace_diagram::Shape::Connector { from, to, .. }) = shape_restored else {
panic!("expected connector");
};
assert_eq!(from.terminal, monospace_core::Terminal::Arm);
assert_eq!(
to.terminal,
monospace_core::Terminal::Symbol {
style: monospace_core::SymbolStyleName::from("solid")
}
);
}
#[test]
fn pressing_r_on_selected_line_toggles_orientation_and_updates_status() {
let fixture = r#"{
"next_id": 2,
"shapes": [ { "kind": "line", "id": 1, "at": { "x": 1, "y": 1 },
"len": 5, "orientation": "vertical", "stroke": "light" } ]
}"#;
let mut state = a_drawn_state_over(fixture);
let id = ShapeId::new(NonZeroU16::new(1).unwrap());
state.diagram_mut().select(Some(id));
assert_eq!(
crate::status::status_line(state.diagram()),
"Line #1 · Pos: (1, 1) · Length: 5 · Orientation: Vertical · Stroke: light"
);
assert!(
crate::status::command_line(state.diagram()).contains("RESIZE: Shift+↑↓ · ROTATE: [r]")
);
let r_key = Event::Key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE));
let outcome = send(&r_key, &mut state);
assert!(matches!(outcome, Control::Changed));
let shape = state.diagram().session().diagram().get(id);
let Some(monospace_diagram::Shape::Line {
orientation,
at,
len,
stroke,
}) = shape
else {
panic!("expected line");
};
assert_eq!(*orientation, monospace_core::Orientation::Horizontal);
assert_eq!(*at, monospace_core::Pos { x: 1, y: 1 });
assert_eq!(*len, 5);
assert_eq!(*stroke, monospace_diagram::Stroke::from("light"));
assert_eq!(
crate::status::status_line(state.diagram()),
"Line #1 · Pos: (1, 1) · Length: 5 · Orientation: Horizontal · Stroke: light"
);
assert!(
crate::status::command_line(state.diagram()).contains("RESIZE: Shift+←→ · ROTATE: [r]")
);
assert!(state.diagram().session().can_undo());
let outcome2 = send(&r_key, &mut state);
assert!(matches!(outcome2, Control::Changed));
assert_eq!(
crate::status::status_line(state.diagram()),
"Line #1 · Pos: (1, 1) · Length: 5 · Orientation: Vertical · Stroke: light"
);
let undo_key = Event::Key(KeyEvent::new(KeyCode::Char('z'), KeyModifiers::CONTROL));
let undo_outcome = send(&undo_key, &mut state);
assert!(matches!(undo_outcome, Control::Changed));
let shape_restored = state.diagram().session().diagram().get(id);
let Some(monospace_diagram::Shape::Line { orientation, .. }) = shape_restored else {
panic!("expected line");
};
assert_eq!(*orientation, monospace_core::Orientation::Horizontal);
}
#[test]
fn enter_remains_ignored_on_a_box() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let outcome = send(&key(KeyCode::Enter), &mut state);
assert!(matches!(outcome, Control::Continue), "{outcome:?}");
assert!(!state.text().is_up(), "Enter opened the editor");
}
#[test]
fn ok_saves_and_escape_discards() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let _ = send(&key(KeyCode::Char('t')), &mut state);
for character in ['h', 'i'] {
let _ = send(&key(KeyCode::Char(character)), &mut state);
}
let _ = send(&key(KeyCode::Tab), &mut state);
let outcome = send(&key(KeyCode::Enter), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(!state.text().is_up(), "Ok did not close the editor");
assert!(state.diagram().can_undo(), "saving recorded no step");
assert_eq!(
state.diagram().selected(),
Some(ShapeId::new(NonZeroU16::new(1).unwrap())),
"saving dropped the selection"
);
let _ = send(&key(KeyCode::Char('t')), &mut state);
let _ = send(&key(KeyCode::Char('x')), &mut state);
let outcome = send(&key(KeyCode::Esc), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(!state.text().is_up(), "Escape did not close the editor");
let _ = send(&key(KeyCode::Char('t')), &mut state);
let _ = send(&key(KeyCode::Char('y')), &mut state);
let _ = send(&key(KeyCode::Tab), &mut state);
let _ = send(&key(KeyCode::Tab), &mut state);
let outcome = send(&key(KeyCode::Enter), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(
!state.text().is_up(),
"Enter on Cancel did not close the editor"
);
let Shape::Box { text: kept, .. } = state
.diagram()
.session()
.diagram()
.get(ShapeId::new(NonZeroU16::new(1).unwrap()))
.expect("the box is there")
else {
panic!("expected box");
};
assert_eq!(
*kept,
Some(Text::new("hi").unwrap()),
"Cancel saved the buffer"
);
}
#[test]
fn saving_the_same_text_records_no_step() {
let mut state = a_drawn_state_over(A_BOX_WITH_TEXT);
let _ = send(&click(3, 3), &mut state);
let _ = send(&key(KeyCode::Char('t')), &mut state);
let _ = send(&key(KeyCode::Tab), &mut state);
let outcome = send(&key(KeyCode::Enter), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(!state.text().is_up(), "Ok did not close the editor");
assert!(
!state.diagram().can_undo(),
"saving the same text recorded a step"
);
}
#[test]
fn ctrl_c_copies_while_the_text_editor_is_up() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let _ = send(&key(KeyCode::Char('t')), &mut state);
for character in ['h', 'i'] {
let _ = send(&key(KeyCode::Char(character)), &mut state);
}
let _ = send(
&key_with(KeyCode::Char('a'), KeyModifiers::CONTROL),
&mut state,
);
let outcome = send(
&key_with(KeyCode::Char('c'), KeyModifiers::CONTROL),
&mut state,
);
assert!(matches!(outcome, Control::Continue), "{outcome:?}");
assert!(state.text().is_up(), "Ctrl+C closed the editor");
assert_eq!(
rat_widget::text::clipboard::global_clipboard()
.get_string()
.as_deref()
.ok(),
Some("hi")
);
}
#[test]
fn the_editor_consumes_every_key() {
for code in [
KeyCode::Char('q'),
KeyCode::Char('s'),
KeyCode::Char('o'),
KeyCode::Char('n'),
] {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let _ = send(&key(KeyCode::Char('t')), &mut state);
let outcome = send(&key_with(code, KeyModifiers::CONTROL), &mut state);
assert!(
matches!(outcome, Control::Continue),
"{code:?}: {outcome:?}"
);
assert!(state.text().is_up(), "{code:?}: the editor closed");
assert!(
!state.dialog().is_up(),
"{code:?}: the message dialog opened"
);
assert!(!state.open().is_up(), "{code:?}: the open dialog opened");
assert!(!state.save().is_up(), "{code:?}: the save dialog opened");
assert!(!state.diagram().can_undo(), "{code:?}: the diagram changed");
}
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let _ = send(&key(KeyCode::Char('t')), &mut state);
let outcome = send(&key(KeyCode::Char('t')), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(state.text().is_up(), "t inside the editor closed it");
assert_eq!(
state.text().get().expect("the text editor is up").buffer(),
"t"
);
let outcome = send(&key(KeyCode::Tab), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(state.text().is_up(), "Tab closed the editor");
assert_eq!(
state.text().get().expect("the text editor is up").buffer(),
"t",
"Tab inserted a tab"
);
let leaf = state.chrome().leaf();
let outcome = send(&click(3, 0), &mut state);
assert!(matches!(outcome, Control::Continue), "{outcome:?}");
assert!(state.text().is_up(), "a click on the bar closed the editor");
assert_eq!(state.chrome().leaf(), leaf, "the click moved the keyboard");
}
#[test]
fn taking_an_entry_with_nothing_to_give_back_changes_nothing() {
let mut state = a_drawn_state_over(A_BOX);
let before = draw(&mut state);
let outcome = take_the_entry(&mut state, 'e', "Undo");
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(
!state.chrome().popup_active(),
"the entry left the menu open"
);
assert!(!state.diagram().can_undo(), "the entry recorded a step");
assert!(!state.diagram().can_redo(), "the entry left a step ahead");
let after = draw(&mut state);
for line in 0..HEIGHT {
assert_eq!(
row_of(&before, line),
row_of(&after, line),
"row {line} is not what it was"
);
}
let outcome = take_the_entry(&mut state, 'e', "Redo");
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(
!state.chrome().popup_active(),
"the entry left the menu open"
);
assert!(
!state.diagram().can_undo(),
"the redo entry recorded a step"
);
assert!(
!state.diagram().can_redo(),
"the redo entry left a step ahead"
);
}
#[test]
fn the_undo_key_gives_the_step_back() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let _ = send(&key(KeyCode::Delete), &mut state);
let _ = draw(&mut state);
assert!(
state.diagram().owner(Offset { x: 2, y: 1 }).is_none(),
"the box was not removed, so the test proves nothing"
);
let outcome = send(&undo_key(), &mut state);
let buffer = draw(&mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(
state.diagram().owner(Offset { x: 2, y: 1 }).is_some(),
"the undo did not give the box back"
);
assert_eq!(
buffer[(3, 3)].symbol(),
"┌",
"the frame after the undo does not draw the box"
);
assert!(!state.diagram().can_undo(), "the undo left a step behind");
assert!(state.diagram().can_redo(), "the undo left nothing to redo");
}
#[test]
fn the_redo_key_gives_it_forward_again() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let _ = send(&key(KeyCode::Delete), &mut state);
let _ = send(&undo_key(), &mut state);
let _ = draw(&mut state);
assert!(
state.diagram().owner(Offset { x: 2, y: 1 }).is_some(),
"the undo did not give the box back, so the test proves nothing"
);
let outcome = send(&redo_key(), &mut state);
let buffer = draw(&mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(
state.diagram().owner(Offset { x: 2, y: 1 }).is_none(),
"the redo did not take the box out again"
);
assert_eq!(
buffer[(3, 3)].symbol(),
" ",
"the frame after the redo still draws the box"
);
assert!(state.diagram().can_undo(), "the redo left nothing to undo");
assert!(!state.diagram().can_redo(), "the redo left a step ahead");
}
#[test]
fn the_undo_key_with_nothing_behind_changes_nothing() {
let mut state = a_drawn_state_over(A_BOX);
let outcome = send(&undo_key(), &mut state);
let buffer = draw(&mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(!state.diagram().can_undo(), "the press recorded a step");
assert!(!state.diagram().can_redo(), "the press left a step ahead");
assert_eq!(
buffer[(3, 3)].symbol(),
"┌",
"the press changed the drawing"
);
}
#[test]
fn the_redo_key_with_nothing_ahead_changes_nothing() {
let mut state = a_drawn_state_over(A_BOX);
let outcome = send(&redo_key(), &mut state);
let buffer = draw(&mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(!state.diagram().can_undo(), "the press recorded a step");
assert!(!state.diagram().can_redo(), "the press left a step ahead");
assert_eq!(
buffer[(3, 3)].symbol(),
"┌",
"the press changed the drawing"
);
}
#[test]
fn the_walks_are_answered_with_the_drop_down_open_and_take_the_keyboard_back() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let _ = send(&key(KeyCode::Delete), &mut state);
let _ = draw(&mut state);
assert!(
state.diagram().owner(Offset { x: 2, y: 1 }).is_none(),
"the box was not removed, so the test proves nothing"
);
open_the_drop_down(&mut state, 'f');
let outcome = send(&undo_key(), &mut state);
let _ = draw(&mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(
state.diagram().owner(Offset { x: 2, y: 1 }).is_some(),
"the drop-down kept the undo key from the history"
);
assert_eq!(
state.chrome().leaf(),
Focus::Region,
"the undo left the keyboard on the bar"
);
assert!(
!state.chrome().popup_active(),
"a drop-down is open with the keyboard on the region"
);
open_the_drop_down(&mut state, 'f');
let outcome = send(&redo_key(), &mut state);
let _ = draw(&mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(
state.diagram().owner(Offset { x: 2, y: 1 }).is_none(),
"the drop-down kept the redo key from the history"
);
assert_eq!(
state.chrome().leaf(),
Focus::Region,
"the redo left the keyboard on the bar"
);
assert!(
!state.chrome().popup_active(),
"a drop-down is open with the keyboard on the region"
);
}
#[test]
fn the_undo_shortcut_says_only_that_it_undoes() {
assert_eq!(
shortcuts::handle_unclaimed(&undo_key()),
Reply::Act(Action::Undo),
"the undo key is not a row of the table, or says something else"
);
assert_eq!(
shortcuts::handle_always(&undo_key()),
Reply::Ignored,
"the undo key reaches before the chrome"
);
}
#[test]
fn the_redo_shortcut_says_only_that_it_redoes() {
assert_eq!(
shortcuts::handle_unclaimed(&redo_key()),
Reply::Act(Action::Redo),
"the redo key is not a row of the table, or says something else"
);
assert_eq!(
shortcuts::handle_always(&redo_key()),
Reply::Ignored,
"the redo key reaches before the chrome"
);
}
#[test]
fn the_undo_and_redo_keys_clear_the_selection() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let _ = send(&key(KeyCode::Right), &mut state);
assert!(
state.diagram().selected().is_some(),
"the box was not selected"
);
let outcome = send(&undo_key(), &mut state);
let buffer = draw(&mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(
state.diagram().selected(),
None,
"the undo kept the selection"
);
assert!(
!reversed(&buffer, 3, 3),
"the frame after the undo inverts a cell"
);
let _ = send(&click(3, 3), &mut state);
assert!(
state.diagram().selected().is_some(),
"the box was not selected again"
);
let outcome = send(&redo_key(), &mut state);
let buffer = draw(&mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(
state.diagram().selected(),
None,
"the redo kept the selection"
);
assert!(
!reversed(&buffer, 4, 3),
"the frame after the redo inverts a cell"
);
}
#[test]
fn a_frame_after_an_undo_draws_the_restored_diagram() {
let mut state = a_drawn_state_over(A_BOX);
let _ = send(&click(3, 3), &mut state);
let before = draw(&mut state);
let _ = send(&key(KeyCode::Delete), &mut state);
let _ = send(&undo_key(), &mut state);
let after = draw(&mut state);
let region = frame::regions(Rect::new(0, 0, WIDTH, HEIGHT)).region;
for line in region.top()..region.bottom() {
assert_eq!(
row_of(&before, line),
row_of(&after, line),
"row {line} is not where the step left it"
);
}
}
#[test]
fn a_frame_after_a_removal_draws_the_connector_where_it_stood() {
let mut state = a_drawn_state_over(A_BOX_AND_WHAT_HANGS_FROM_IT);
let _ = send(&click(1, 3), &mut state);
assert!(
state.diagram().selected().is_some(),
"the left box was not selected"
);
let _ = send(&key(KeyCode::Delete), &mut state);
let buffer = draw(&mut state);
for (column, line) in [(1, 3), (4, 3), (1, 4), (1, 5), (4, 5)] {
assert_eq!(
buffer[(column, line)].symbol(),
" ",
"({column}, {line}) still holds the removed box"
);
}
assert_eq!(
row_of(&buffer, 4),
"│ ─────────────┤ │ │",
"the frame does not draw the connector where it stood"
);
assert_eq!(
buffer[(17, 3)].symbol(),
"┌",
"the right box went with the connector"
);
assert_eq!(
buffer[(20, 3)].symbol(),
"┐",
"the right box is not four cells wide"
);
}
#[test]
fn the_wheel_moves_the_view_by_the_helpers_step() {
let mut state = a_drawn_state_over(A_BOX_TOO_BIG);
let outcome = send(&wheel(MouseEventKind::ScrollDown, 5, 4), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(
state.diagram().origin(),
Pos { x: 0, y: 1 },
"a turn down did not move the view by a tenth of the window, at least one cell"
);
let outcome = send(
&wheel_with(MouseEventKind::ScrollDown, KeyModifiers::ALT, 5, 4),
&mut state,
);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(
state.diagram().origin(),
Pos { x: 3, y: 1 },
"Alt and a turn down did not move the view across by a tenth of the window"
);
}
#[test]
fn a_wheel_at_the_end_moves_nothing_and_owes_no_frame() {
let mut state = a_drawn_state_over(A_BOX_TOO_BIG);
for _ in 0..8 {
let _ = send(
&wheel_with(MouseEventKind::ScrollDown, KeyModifiers::ALT, 5, 4),
&mut state,
);
}
assert_eq!(
state.diagram().origin(),
Pos { x: 22, y: 0 },
"eight turns did not reach the content's far end, so the test proves nothing"
);
let outcome = send(
&wheel_with(MouseEventKind::ScrollDown, KeyModifiers::ALT, 5, 4),
&mut state,
);
assert!(
matches!(outcome, Control::Continue),
"the end of the content owed a frame: {outcome:?}"
);
assert_eq!(
state.diagram().origin(),
Pos { x: 22, y: 0 },
"the view moved past the content"
);
}
#[test]
fn a_press_on_the_bar_jumps_the_view() {
let mut state = a_drawn_state_over(A_BOX_TOO_BIG);
let outcome = send(&click(39, 5), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(
state.diagram().origin(),
Pos { x: 0, y: 6 },
"the press did not take the view to the position it named"
);
}
#[test]
fn a_drag_on_the_bar_follows_the_pointer() {
let mut state = a_drawn_state_over(A_BOX_TOO_BIG);
let outcome = send(&click(39, 3), &mut state);
assert!(matches!(outcome, Control::Continue), "{outcome:?}");
assert_eq!(
state.diagram().origin(),
Pos { x: 0, y: 0 },
"the press where the thumb stands moved the view"
);
let outcome = send(&drag(39, 5), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(
state.diagram().origin(),
Pos { x: 0, y: 6 },
"the drag did not follow the pointer down the track"
);
let outcome = send(&drag(39, 6), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(
state.diagram().origin(),
Pos { x: 0, y: 9 },
"the drag across the track's last cell did not reach the content's end"
);
}
#[test]
fn a_drag_past_the_track_is_clamped_to_the_content() {
let mut state = a_drawn_state_over(A_BOX_TOO_BIG);
let outcome = send(&click(39, 3), &mut state);
assert!(
matches!(outcome, Control::Continue),
"the press moved the view: {outcome:?}"
);
let outcome = send(&drag(39, 7), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(
state.diagram().origin(),
Pos { x: 0, y: 9 },
"the drag past the track did not stop at the content's end"
);
}
#[test]
fn a_scroll_records_no_step() {
let mut state = a_drawn_state_over(A_BOX_TOO_BIG);
let reach = state.diagram().footprint();
assert!(reach.is_some(), "the drawing reached nothing");
let outcome = send(&wheel(MouseEventKind::ScrollDown, 5, 4), &mut state);
assert!(
matches!(outcome, Control::Changed),
"the view did not move, so the test proves nothing: {outcome:?}"
);
assert!(!state.diagram().can_undo(), "the scroll recorded a step");
assert!(!state.diagram().can_redo(), "the scroll left a step ahead");
assert_eq!(
state.diagram().footprint(),
reach,
"the scroll changed what the drawing reaches"
);
}
#[test]
fn neither_an_undo_nor_a_redo_restores_a_view() {
let mut state = a_drawn_state_over(A_BOX_TOO_BIG);
let _ = send(&click(2, 2), &mut state);
assert!(
state.diagram().selected().is_some(),
"the box was not selected"
);
let _ = send(&key(KeyCode::Right), &mut state);
assert!(state.diagram().can_undo(), "the move recorded no step");
let _ = send(&wheel(MouseEventKind::ScrollDown, 5, 4), &mut state);
assert_eq!(
state.diagram().origin(),
Pos { x: 0, y: 1 },
"the wheel did not move the view, so the test proves nothing"
);
let outcome = send(&undo_key(), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(
state.diagram().can_redo(),
"the undo did not walk the history"
);
assert_eq!(
state.diagram().origin(),
Pos { x: 0, y: 1 },
"the undo restored a view"
);
let outcome = send(&redo_key(), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert!(
state.diagram().can_undo(),
"the redo did not walk the history"
);
assert_eq!(
state.diagram().origin(),
Pos { x: 0, y: 1 },
"the redo restored a view"
);
}
#[test]
fn a_scroll_keeps_the_selection() {
let mut state = a_drawn_state_over(A_BOX_TOO_BIG);
let _ = send(&click(2, 2), &mut state);
let selected = state.diagram().selected();
assert!(selected.is_some(), "the box was not selected");
assert!(
reversed(&draw(&mut state), 2, 2),
"the selection is not inverted before the scroll"
);
let outcome = send(&wheel(MouseEventKind::ScrollDown, 5, 4), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(
state.diagram().origin(),
Pos { x: 0, y: 1 },
"the wheel did not move the view, so the test proves nothing"
);
let buffer = draw(&mut state);
let region = frame::regions(Rect::new(0, 0, WIDTH, HEIGHT)).region;
assert_eq!(
state.diagram().selected(),
selected,
"the scroll dropped the selection"
);
for y in 0..region.height - 2 {
for x in 0..region.width - 2 {
let at = Offset { x, y };
assert_eq!(
reversed(&buffer, region.left() + 1 + x, region.top() + 1 + y),
state.diagram().is_cell_selected(at),
"the cell at {at:?} is not the inversion the drawing records"
);
}
}
}
#[test]
fn a_click_after_a_scroll_selects_the_cell_under_the_pointer() {
let mut state = a_drawn_state_over(BOXES_APART);
let outcome = send(
&wheel_with(MouseEventKind::ScrollDown, KeyModifiers::ALT, 20, 4),
&mut state,
);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(
state.diagram().origin(),
Pos { x: 3, y: 0 },
"the view did not move across, so the test proves nothing"
);
let _ = draw(&mut state);
let outcome = send(&click(25, 2), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
let under = state.diagram().owner(Offset { x: 24, y: 0 });
assert!(
under.is_some(),
"the cell under the pointer is not the middle box's corner"
);
assert_eq!(
state.diagram().selected(),
under,
"the click selected another shape"
);
}
#[test]
fn a_press_on_the_bar_gives_the_region_the_keyboard() {
let mut state = a_drawn_state_over(A_BOX_TOO_BIG);
let _ = send(&click(2, 2), &mut state);
let selected = state.diagram().selected();
assert!(selected.is_some(), "the box was not selected");
state.chrome_mut().move_to_bar();
let _ = draw(&mut state);
let outcome = send(&click(39, 5), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
assert_eq!(
state.chrome().leaf(),
Focus::Region,
"the press on the bar left the keyboard on the bar"
);
assert_eq!(
state.diagram().selected(),
selected,
"the press on the bar changed the selection"
);
assert_eq!(
state.diagram().origin(),
Pos { x: 0, y: 6 },
"the press on the bar did not move the view"
);
}
#[test]
fn a_resize_repaints_although_nobody_asked() {
let mut state = a_drawn_state();
let outcome = send(&Event::Resize(80, 24), &mut state);
assert!(matches!(outcome, Control::Changed), "{outcome:?}");
}
#[test]
fn the_error_handler_does_not_answer_with_the_error_again() {
let mut state = a_state();
let mut global = a_global();
let failure: AppError = "no terminal".into();
let outcome = error(failure, &mut state, &mut global);
assert!(
matches!(outcome, Ok(Control::Quit)),
"the handler answered with the error again: {outcome:?}"
);
assert!(
global.take_failure().is_some(),
"the error was not kept to be written out"
);
}
#[test]
fn the_loop_set_the_terminal_up_itself_so_there_is_nothing_to_init() {
assert!(init(&mut a_state(), &mut a_global()).is_ok());
}
#[test]
fn pressing_d_duplicates_selected_shape_selects_new_id_and_undoes_in_one_step() {
let fixture = r#"{
"next_id": 2,
"shapes": [ { "kind": "box", "id": 1, "at": { "x": 1, "y": 1 },
"size": { "width": 6, "height": 3 }, "stroke": "double", "text": "API" } ]
}"#;
let mut state = a_drawn_state_over(fixture);
let id1 = ShapeId::new(NonZeroU16::new(1).unwrap());
let id2 = ShapeId::new(NonZeroU16::new(2).unwrap());
let d_key = Event::Key(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE));
let idle_outcome = send(&d_key, &mut state);
assert!(matches!(idle_outcome, Control::Continue));
assert_eq!(state.diagram().session().diagram().shape_count(), 1);
state.diagram_mut().select(Some(id1));
assert_eq!(
crate::status::status_line(state.diagram()),
"Box #1 \"API\" · Pos: (1, 1) · Size: 6×3 · Stroke: double · Fill: None"
);
assert!(
crate::status::command_line(state.diagram())
.ends_with("· [d] Duplicate · [Del] Delete · [Esc] Deselect")
);
let dup_outcome = send(&d_key, &mut state);
assert!(matches!(dup_outcome, Control::Changed));
assert_eq!(state.diagram().session().diagram().shape_count(), 2);
assert_eq!(state.diagram().selected(), Some(id2));
assert_eq!(
crate::status::status_line(state.diagram()),
"Box #2 \"API\" · Pos: (1, 1) · Size: 6×3 · Stroke: double · Fill: None"
);
let undo_key = Event::Key(KeyEvent::new(KeyCode::Char('z'), KeyModifiers::CONTROL));
let undo_outcome = send(&undo_key, &mut state);
assert!(matches!(undo_outcome, Control::Changed));
assert_eq!(state.diagram().session().diagram().shape_count(), 1);
assert_eq!(state.diagram().selected(), None);
assert!(!state.diagram().session().can_undo());
let redo_key = Event::Key(KeyEvent::new(
KeyCode::Char('Z'),
KeyModifiers::CONTROL.union(KeyModifiers::SHIFT),
));
let redo_outcome = send(&redo_key, &mut state);
assert!(matches!(redo_outcome, Control::Changed));
assert_eq!(state.diagram().session().diagram().shape_count(), 2);
assert!(state.diagram().session().diagram().get(id2).is_some());
}
}