use super::*;
use crate::testing::*;
fn setup() -> (World, Entity, Target, Entity) {
let mut world = World::new();
navigation::observe(&mut world);
let workspace = world.spawn(Workspace).id();
let tab = world.spawn((Tab, ChildOf(workspace))).id();
let pane = world.spawn_empty().id();
let leaf = world.spawn((PaneView { pane }, ChildOf(tab))).id();
let other = world.spawn((PaneView { pane }, ChildOf(tab))).id();
let id = world
.spawn((
Viewer {
rows: 24,
cols: 80,
zoom: false,
scrollback: 0,
notice: None,
},
Viewing(workspace),
OnTab(tab),
Focused(leaf),
))
.id();
(
world,
id,
Target {
workspace,
tab: Some(tab),
leaf: Some(leaf),
},
other,
)
}
fn key(key: &str) -> Input {
Input::Key {
key: key.parse().unwrap_or(Key::F(12)),
modifiers: Modifiers::default(),
}
}
#[test]
fn menus_keep_unbound_actions_and_share_navigation_including_layout_prompts()
-> crate::testing::Outcome {
let (mut world, id, target, _) = setup();
for (menu, required) in [
(
Action::PaneMenu,
vec![
Action::Terminate,
Action::ReorderPrev,
Action::ReorderNext,
Action::MoveWorkspace,
Action::ScrollUp,
Action::ScrollDown,
Action::SwapChoose,
Action::MoveTab,
Action::MoveNewTab,
Action::MoveNewWorkspace,
],
),
(
Action::TabMenu,
vec![
Action::RenameTab,
Action::TabClose,
Action::TabReorderPrevious,
Action::TabReorderNext,
],
),
(
Action::WorkspaceMenu,
vec![
Action::RenameWorkspace,
Action::WorkspaceClose,
Action::WorkspaceReorderPrevious,
Action::WorkspaceReorderNext,
Action::SaveLayout,
Action::LoadLayout,
],
),
] {
bound(&mut world, id, target, menu)?;
let Mode::List { entries, .. } = &world.get::<Overlay>(id).need()?.mode else {
return Err("unexpected overlay mode".into());
};
for action in required {
assert!(
entries
.iter()
.any(|e| matches!(e.run, Run::Action(a) if a == action)),
"{menu:?} lacks {action:?}"
);
}
let count = entries.len();
for rows in 0..8 {
world.get_mut::<Viewer>(id).need()?.rows = rows;
input(&mut world, id, &key("home"));
input(&mut world, id, &key("pagedown"));
let Mode::List { selected, .. } = &world.get::<Overlay>(id).need()?.mode else {
return Err("unexpected overlay mode".into());
};
assert_eq!(*selected, list_capacity(rows).min(count - 1));
input(&mut world, id, &key("end"));
let rendered = lines(&world, world.get::<Overlay>(id).need()?, rows);
if rows >= 2 {
assert!(
rendered
.iter()
.any(|(text, style)| text.starts_with('›') && style.contains('7'))
);
}
input(&mut world, id, &key("pageup"));
let Mode::List { selected, .. } = &world.get::<Overlay>(id).need()?.mode else {
return Err("unexpected overlay mode".into());
};
assert_eq!(*selected, (count - 1).saturating_sub(list_capacity(rows)));
}
input(&mut world, id, &key("escape"));
assert!(world.get::<Overlay>(id).is_none());
}
for action in [Action::SaveLayout, Action::LoadLayout] {
bound(&mut world, id, target, Action::WorkspaceMenu)?;
let mut overlay = world.get_mut::<Overlay>(id).need()?;
let Mode::List {
entries, selected, ..
} = &mut overlay.mode
else {
return Err("unexpected overlay mode".into());
};
*selected = entries
.iter()
.position(|e| matches!(e.run, Run::Action(a) if a == action))
.need()?;
input(&mut world, id, &key("enter"));
assert!(
matches!(&world.get::<Overlay>(id).need()?.mode, Mode::Text { action: a, .. } if *a == action)
);
input(&mut world, id, &key("escape"));
}
Ok(())
}
#[test]
fn confirmation_captures_target_and_preserves_a_shared_process() -> crate::testing::Outcome {
let (mut world, id, target, other) = setup();
let process = world.get::<PaneView>(other).need()?.pane;
bound(&mut world, id, target, Action::Close)?;
world.entity_mut(id).insert(Focused(other));
assert!(input(&mut world, id, &Input::Paste { text: "y".into() }));
assert!(world.get_entity(target.leaf.need()?).is_ok());
assert!(input(&mut world, id, &key("y")));
assert!(world.get_entity(target.leaf.need()?).is_err());
assert!(world.get_entity(other).is_ok());
assert!(world.get_entity(process).is_ok());
close(&mut world, other);
assert!(world.get_entity(process).is_err());
Ok(())
}
#[test]
fn removed_confirmation_target_cancels_without_retargeting() -> crate::testing::Outcome {
let (mut world, id, target, other) = setup();
bound(&mut world, id, target, Action::Close)?;
world.despawn(target.leaf.need()?);
world.entity_mut(id).insert(Focused(other));
assert!(input(&mut world, id, &key("y")));
assert!(world.get_entity(other).is_ok());
assert!(world.get::<Overlay>(id).is_none());
assert!(
world
.get::<Viewer>(id)
.need()?
.notice
.as_ref()
.is_some_and(|n| n.text.contains("cancelled"))
);
Ok(())
}
#[test]
fn tab_close_keeps_an_empty_tab_and_workspace_close_repairs_other_viewers()
-> crate::testing::Outcome {
let (mut world, id, target, _) = setup();
crate::server::execute(
&mut world,
id,
Command::Close {
subject: Subject::Tab(target.tab.need()?),
},
)?;
assert_ne!(on_tab(&world, id), target.tab);
assert!(focused(&world, id).is_none());
assert!(on_tab(&world, id).is_some());
let target = Target::of(&world, id).need()?;
let replacement = world.spawn(Workspace).id();
crate::server::execute(
&mut world,
id,
Command::Close {
subject: Subject::Workspace(target.workspace),
},
)?;
assert_eq!(viewing(&world, id), Some(replacement));
let target = Target::of(&world, id).need()?;
crate::server::execute(
&mut world,
id,
Command::Close {
subject: Subject::Workspace(target.workspace),
},
)?;
assert!(world.get_entity(id).is_err());
Ok(())
}
#[test]
fn rearrangement_keeps_entity_identity_and_native_child_order() -> crate::testing::Outcome {
let (mut world, id, target, other) = setup();
let source = target.leaf.need()?;
swap(&mut world, source, other)?;
assert_eq!(
navigation::leaves(&world, target.tab.need()?),
vec![other, source]
);
move_beside(&mut world, source, other, Direction::Up)?;
let split = world.get::<ChildOf>(source).need()?.parent();
assert!(world.get::<Split>(split).is_some());
assert_eq!(
world
.get::<Children>(split)
.need()?
.iter()
.collect::<Vec<_>>(),
vec![source, other]
);
crate::server::execute(
&mut world,
id,
Command::Move {
to: MoveTo::NewWorkspace {
name: Some("destination".into()),
},
},
)?;
assert_ne!(viewing(&world, id), Some(target.workspace));
assert_eq!(focused(&world, id), Some(source));
assert!(world.get::<PaneView>(other).is_some());
Ok(())
}
#[test]
fn rename_prompt_keeps_its_original_target_and_escape_cancels() -> crate::testing::Outcome {
let (mut world, id, target, other) = setup();
bound(&mut world, id, target, Action::RenameTab)?;
world.entity_mut(id).insert(Focused(other));
input(
&mut world,
id,
&Input::Paste {
text: "界 tab".into(),
},
);
input(&mut world, id, &key("enter"));
assert_eq!(
world.get::<Name>(target.tab.need()?).need()?.as_str(),
"界 tab"
);
bound(&mut world, id, target, Action::Close)?;
input(&mut world, id, &key("escape"));
assert!(world.get::<Overlay>(id).is_none());
assert!(world.get_entity(target.leaf.need()?).is_ok());
Ok(())
}
#[test]
fn each_close_scope_requires_confirmation_and_cancel_keeps_its_hierarchy() -> crate::testing::Outcome
{
for action in [Action::Close, Action::TabClose, Action::WorkspaceClose] {
let (mut world, id, target, _) = setup();
bound(&mut world, id, target, action)?;
assert!(world.get::<Overlay>(id).is_some());
input(&mut world, id, &Input::Paste { text: "y".into() });
assert!(world.get_entity(target.leaf.need()?).is_ok());
input(&mut world, id, &key("n"));
assert!(world.get_entity(target.leaf.need()?).is_ok());
bound(&mut world, id, target, action)?;
input(&mut world, id, &key("y"));
assert!(world.get_entity(target.leaf.need()?).is_err());
assert_eq!(
world.get_entity(target.workspace).is_err(),
action == Action::WorkspaceClose
);
}
Ok(())
}
#[test]
fn stale_chooser_destination_never_changes_the_source_or_an_unrelated_target()
-> crate::testing::Outcome {
let (mut world, id, target, _) = setup();
let destination = world.spawn((Tab, ChildOf(target.workspace))).id();
let overlay = Overlay {
serial: 1,
target,
mode: Mode::List {
title: "move".into(),
selected: 0,
entries: vec![Entry {
label: "removed".into(),
run: Run::Command(Command::Move {
to: MoveTo::Tab { tab: destination },
}),
}],
},
};
world.entity_mut(id).insert(overlay);
world.despawn(destination);
input(&mut world, id, &key("enter"));
assert_eq!(
world.get::<ChildOf>(target.leaf.need()?).need()?.parent(),
target.tab.need()?
);
assert!(
world
.get::<Viewer>(id)
.need()?
.notice
.as_ref()
.is_some_and(|n| n.text.contains("removed"))
);
Ok(())
}
#[test]
fn every_chooser_entry_remains_visible_on_tiny_terminals() -> crate::testing::Outcome {
let (world, _, target, _) = setup();
let tab = target.tab.need()?;
for rows in 0..10 {
for selected in 0..10 {
let entries = (0..10)
.map(|i| Entry {
label: format!("entry-{i}"),
run: Run::Command(Command::Select {
scope: Scope::Tab,
entity: tab,
}),
})
.collect();
let overlay = Overlay {
serial: 1,
target,
mode: Mode::List {
title: "Choose".into(),
entries,
selected,
},
};
let lines = lines(&world, &overlay, rows);
assert!(lines.len() <= rows.saturating_sub(1) as usize);
if rows >= 2 {
assert!(
lines
.iter()
.any(|(s, _)| s.contains(&format!("entry-{selected}"))),
"{rows}: {lines:?}"
);
}
}
}
Ok(())
}
#[test]
fn pasted_text_cannot_cross_a_cancelled_or_reopened_prompt() -> crate::testing::Outcome {
let (mut world, id, target, _) = setup();
bound(&mut world, id, target, Action::RenameTab)?;
assert!(crate::paste::input(&mut world, id, &Input::PasteBegin));
input(&mut world, id, &key("escape"));
bound(&mut world, id, target, Action::RenameTab)?;
assert!(crate::paste::input(
&mut world,
id,
&Input::Paste {
text: "old prompt text".into()
}
));
let Mode::Text { buffer, .. } = &world.get::<Overlay>(id).need()?.mode else {
return Err("unexpected overlay mode".into());
};
assert!(buffer.is_empty());
Ok(())
}