use super::*;
mod doctrine;
pub(super) const JOIN_STATES: [JoinState; 7] = [
JoinState::ReadyStartable,
JoinState::Blocked,
JoinState::Bound,
JoinState::ClaimedElsewhere,
JoinState::Delivered,
JoinState::UnassignedWorkspace,
JoinState::OrphanedProject,
];
fn verbs(seat: Seat) -> Vec<Verb> {
let mut out = Vec::new();
for entry in entries(seat) {
match entry.action {
Action::Fire(verb) => out.push(verb),
Action::Submenu(children) => {
for child in children {
if let Action::Fire(verb) = child.action {
out.push(verb);
}
}
}
}
}
out
}
#[test]
fn a_named_pinned_tab_carries_delete_then_unpin() {
let seat = Seat::WorkspaceTab {
named: true,
pinned: true,
};
assert_eq!(verbs(seat.clone()), [Verb::DeleteWorkspace, Verb::Unpin]);
assert_eq!(entries(seat)[0].label, "delete this workspace…");
}
#[test]
fn a_foreign_tab_offers_no_delete_and_an_unpinned_one_no_unpin() {
assert_eq!(
verbs(Seat::WorkspaceTab {
named: false,
pinned: true,
}),
[Verb::Unpin]
);
assert_eq!(
verbs(Seat::WorkspaceTab {
named: true,
pinned: false,
}),
[Verb::DeleteWorkspace]
);
}
#[test]
fn a_foreign_unpinned_tab_has_no_menu_at_all() {
assert!(
entries(Seat::WorkspaceTab {
named: false,
pinned: false,
})
.is_empty()
);
}
#[test]
fn a_live_conversation_with_children_carries_stop_both_ways_then_flush() {
let seat = Seat::ConversationRow {
stoppable: true,
has_children: true,
named: false,
};
assert_eq!(
verbs(seat.clone()),
[
Verb::Stop { children: false },
Verb::Stop { children: true },
Verb::Flush,
]
);
assert_eq!(entries(seat)[1].label, "stop + children");
}
#[test]
fn a_lone_live_conversation_offers_no_children_cascade() {
assert_eq!(
verbs(Seat::ConversationRow {
stoppable: true,
has_children: false,
named: false,
}),
[Verb::Stop { children: false }, Verb::Flush]
);
}
#[test]
fn a_settled_conversation_still_carries_flush() {
for has_children in [false, true] {
assert_eq!(
verbs(Seat::ConversationRow {
stoppable: false,
has_children,
named: false,
}),
[Verb::Flush]
);
}
}
#[test]
fn a_named_workspaces_conversation_carries_delete_last_and_a_foreign_ones_none() {
let seat = Seat::ConversationRow {
stoppable: true,
has_children: true,
named: true,
};
assert_eq!(
verbs(seat.clone()),
[
Verb::Stop { children: false },
Verb::Stop { children: true },
Verb::Flush,
Verb::DeleteAgent,
]
);
assert_eq!(entries(seat)[3].label, "delete this conversation…");
assert_eq!(
verbs(Seat::ConversationRow {
stoppable: false,
has_children: false,
named: true,
}),
[Verb::Flush, Verb::DeleteAgent]
);
}
#[test]
fn a_ready_ball_offers_only_assign_and_only_with_a_focused_workspace() {
assert_eq!(
verbs(Seat::BallRow {
state: JoinState::ReadyStartable,
assign_to: Some("alba-koi".to_owned()),
move_to: vec!["zeta-pug".to_owned()],
}),
[Verb::Assign("alba-koi".to_owned())],
"unclaimed ⇒ nothing to move, release or close"
);
assert!(
entries(Seat::BallRow {
state: JoinState::ReadyStartable,
assign_to: None,
move_to: vec!["zeta-pug".to_owned()],
})
.is_empty(),
"no focused workspace ⇒ no destination ⇒ no menu at all"
);
}
#[test]
fn a_bound_ball_carries_move_release_close_in_roster_order() {
let seat = Seat::BallRow {
state: JoinState::Bound,
assign_to: Some("alba-koi".to_owned()),
move_to: vec!["zeta-pug".to_owned(), "moss-hare".to_owned()],
};
assert_eq!(
verbs(seat.clone()),
[
Verb::MoveTo("zeta-pug".to_owned()),
Verb::MoveTo("moss-hare".to_owned()),
Verb::Release,
Verb::CloseBall,
],
"bound ⇒ no assign; destinations in the caller's order"
);
let rows = entries(seat);
assert_eq!(rows[0].label, "move to");
let Action::Submenu(destinations) = &rows[0].action else {
panic!("Move's destination is a submenu (§11)");
};
let labels: Vec<&str> = destinations.iter().map(|d| d.label.as_str()).collect();
assert_eq!(
labels,
["zeta-pug", "moss-hare"],
"each row names its target"
);
assert_eq!(destinations[0].carrier, rows[0].carrier);
}
#[test]
fn the_only_workspace_holding_a_ball_offers_no_move() {
assert_eq!(
verbs(Seat::BallRow {
state: JoinState::Bound,
assign_to: None,
move_to: Vec::new(),
}),
[Verb::Release, Verb::CloseBall],
"nowhere to move to ⇒ no empty submenu"
);
}
#[test]
fn an_unactionable_ball_row_has_no_menu_at_all() {
for state in JOIN_STATES {
if matches!(state, JoinState::ReadyStartable | JoinState::Bound) {
continue;
}
assert!(
entries(Seat::BallRow {
state,
assign_to: Some("alba-koi".to_owned()),
move_to: vec!["zeta-pug".to_owned()],
})
.is_empty(),
"{state:?} carries no ball verb"
);
}
}