use crate::assertion;
use crate::assertion::{AssertionFailure, Expected, MatchResult};
use crate::frame::TerminalFrame;
use crate::region::Region;
pub fn match_selected_tab(frame: &TerminalFrame, label: &str) -> MatchResult {
let header = Region::top_row(frame.cols());
assertion::match_text_in_region(frame, label, &header)?;
let header_matches = frame.find_text_in_region(label, &header);
let span = &header_matches[0];
if !span.is_highlighted() {
let message = format!(
"Text '{label}' at ({}, {}) is not highlighted. Style: {:?}, fg: {:?}, bg: {:?}",
span.rect.col, span.rect.row, span.style, span.foreground, span.background
);
return Err(Box::new(AssertionFailure {
message,
expected: Expected::Highlighted {
needle: label.to_string(),
},
region: Some(header),
matched_spans: header_matches,
frame_excerpt: frame.all_text(),
}));
}
Ok(())
}
pub fn expect_selected_tab(frame: &TerminalFrame, label: &str) {
let result = match_selected_tab(frame, label);
assert!(
result.is_ok(),
"{}",
result.err().map(|f| f.message).unwrap_or_default()
);
}
pub fn match_unselected_tab(frame: &TerminalFrame, label: &str) -> MatchResult {
let header = Region::top_row(frame.cols());
assertion::match_text_in_region(frame, label, &header)?;
let header_matches = frame.find_text_in_region(label, &header);
let span = &header_matches[0];
if span.is_highlighted() {
let message = format!(
"Text '{label}' at ({}, {}) is highlighted but should not be. Style: {:?}, fg: {:?}, \
bg: {:?}",
span.rect.col, span.rect.row, span.style, span.foreground, span.background
);
return Err(Box::new(AssertionFailure {
message,
expected: Expected::NotHighlighted {
needle: label.to_string(),
},
region: Some(header),
matched_spans: header_matches,
frame_excerpt: frame.all_text(),
}));
}
Ok(())
}
pub fn expect_unselected_tab(frame: &TerminalFrame, label: &str) {
let result = match_unselected_tab(frame, label);
assert!(
result.is_ok(),
"{}",
result.err().map(|f| f.message).unwrap_or_default()
);
}
pub fn match_instruction_visible(frame: &TerminalFrame, instruction: &str) -> MatchResult {
let full = Region::full(frame.cols(), frame.rows());
assertion::match_text_in_region(frame, instruction, &full)
}
pub fn expect_instruction_visible(frame: &TerminalFrame, instruction: &str) {
let result = match_instruction_visible(frame, instruction);
assert!(
result.is_ok(),
"{}",
result.err().map(|f| f.message).unwrap_or_default()
);
}
pub fn match_keybinding_hint(frame: &TerminalFrame, hint: &str) -> MatchResult {
let footer = Region::footer(frame.cols(), frame.rows());
assertion::match_text_in_region(frame, hint, &footer)
}
pub fn expect_keybinding_hint(frame: &TerminalFrame, hint: &str) {
let result = match_keybinding_hint(frame, hint);
assert!(
result.is_ok(),
"{}",
result.err().map(|f| f.message).unwrap_or_default()
);
}
pub fn match_footer_action(frame: &TerminalFrame, action: &str) -> MatchResult {
let footer = Region::footer(frame.cols(), frame.rows());
assertion::match_text_in_region(frame, action, &footer)
}
pub fn expect_footer_action(frame: &TerminalFrame, action: &str) {
let result = match_footer_action(frame, action);
assert!(
result.is_ok(),
"{}",
result.err().map(|f| f.message).unwrap_or_default()
);
}
pub fn match_dialog_title(frame: &TerminalFrame, title: &str) -> MatchResult {
let upper = Region::percent(0, 0, 100, 60, frame.cols(), frame.rows());
assertion::match_text_in_region(frame, title, &upper)
}
pub fn expect_dialog_title(frame: &TerminalFrame, title: &str) {
let result = match_dialog_title(frame, title);
assert!(
result.is_ok(),
"{}",
result.err().map(|f| f.message).unwrap_or_default()
);
}
pub fn match_status_message(frame: &TerminalFrame, message: &str) -> MatchResult {
let full = Region::full(frame.cols(), frame.rows());
assertion::match_text_in_region(frame, message, &full)
}
pub fn expect_status_message(frame: &TerminalFrame, message: &str) {
let result = match_status_message(frame, message);
assert!(
result.is_ok(),
"{}",
result.err().map(|f| f.message).unwrap_or_default()
);
}
pub fn match_not_visible(frame: &TerminalFrame, text: &str) -> MatchResult {
assertion::match_not_visible(frame, text)
}
pub fn expect_not_visible(frame: &TerminalFrame, text: &str) {
let result = match_not_visible(frame, text);
assert!(
result.is_ok(),
"{}",
result.err().map(|f| f.message).unwrap_or_default()
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn expect_selected_tab_passes_for_bold_tab() {
let frame = TerminalFrame::new(80, 24, b"\x1b[1mProjects\x1b[0m Sessions");
expect_selected_tab(&frame, "Projects");
}
#[test]
#[should_panic(expected = "is not highlighted")]
fn expect_selected_tab_panics_when_not_highlighted() {
let frame = TerminalFrame::new(80, 24, b"Projects Sessions");
expect_selected_tab(&frame, "Projects");
}
#[test]
fn expect_unselected_tab_passes_for_plain_tab() {
let frame = TerminalFrame::new(80, 24, b"\x1b[1mProjects\x1b[0m Sessions");
expect_unselected_tab(&frame, "Sessions");
}
#[test]
fn expect_instruction_visible_finds_text() {
let frame = TerminalFrame::new(80, 24, b"\r\n\r\nPress Enter to continue");
expect_instruction_visible(&frame, "Press Enter");
}
#[test]
fn expect_not_visible_passes_when_absent() {
let frame = TerminalFrame::new(80, 24, b"Hello World");
expect_not_visible(&frame, "Goodbye");
}
#[test]
fn expect_status_message_finds_anywhere() {
let mut data = Vec::new();
for _ in 0..10 {
data.extend_from_slice(b"\r\n");
}
data.extend_from_slice(b"Status: OK");
let frame = TerminalFrame::new(80, 24, &data);
expect_status_message(&frame, "Status: OK");
}
#[test]
fn match_selected_tab_returns_ok_for_bold_tab() {
let frame = TerminalFrame::new(80, 24, b"\x1b[1mProjects\x1b[0m Sessions");
let result = match_selected_tab(&frame, "Projects");
assert!(result.is_ok());
}
#[test]
fn match_selected_tab_returns_failure_when_not_highlighted() {
let frame = TerminalFrame::new(80, 24, b"Projects Sessions");
let failure = match_selected_tab(&frame, "Projects").expect_err("should be Err");
assert!(matches!(
&failure.expected,
crate::assertion::Expected::Highlighted { needle, .. } if needle == "Projects"
));
}
#[test]
fn match_selected_tab_returns_failure_when_label_missing_from_header() {
let frame = TerminalFrame::new(80, 24, b"Other Tabs");
let failure = match_selected_tab(&frame, "Projects").expect_err("should be Err");
assert!(matches!(
&failure.expected,
crate::assertion::Expected::TextInRegion { needle, .. } if needle == "Projects"
));
}
#[test]
fn match_unselected_tab_returns_ok_for_plain_tab() {
let frame = TerminalFrame::new(80, 24, b"\x1b[1mProjects\x1b[0m Sessions");
let result = match_unselected_tab(&frame, "Sessions");
assert!(result.is_ok());
}
#[test]
fn match_unselected_tab_returns_failure_when_highlighted() {
let frame = TerminalFrame::new(80, 24, b"\x1b[1mProjects\x1b[0m Sessions");
let failure = match_unselected_tab(&frame, "Projects").expect_err("should be Err");
assert!(matches!(
&failure.expected,
crate::assertion::Expected::NotHighlighted { needle, .. } if needle == "Projects"
));
}
#[test]
fn match_instruction_visible_returns_failure_when_absent() {
let frame = TerminalFrame::new(80, 24, b"Hello");
let failure = match_instruction_visible(&frame, "Press Enter").expect_err("should be Err");
assert!(matches!(
&failure.expected,
crate::assertion::Expected::TextInRegion { needle, .. } if needle == "Press Enter"
));
}
#[test]
fn match_keybinding_hint_returns_ok_when_in_footer() {
let mut data = Vec::new();
for _ in 0..23 {
data.extend_from_slice(b"\r\n");
}
data.extend_from_slice(b"Tab Enter q");
let frame = TerminalFrame::new(80, 24, &data);
let result = match_keybinding_hint(&frame, "Tab");
assert!(result.is_ok());
}
#[test]
fn match_footer_action_returns_failure_when_missing() {
let frame = TerminalFrame::new(80, 24, b"Header text");
let failure = match_footer_action(&frame, "Quit").expect_err("should be Err");
assert!(matches!(
&failure.expected,
crate::assertion::Expected::TextInRegion { needle, .. } if needle == "Quit"
));
}
#[test]
fn match_dialog_title_returns_ok_when_in_upper_region() {
let frame = TerminalFrame::new(80, 24, b"\r\n\r\nConfirm Delete");
let result = match_dialog_title(&frame, "Confirm Delete");
assert!(result.is_ok());
}
#[test]
fn match_status_message_returns_ok_when_present() {
let mut data = Vec::new();
for _ in 0..10 {
data.extend_from_slice(b"\r\n");
}
data.extend_from_slice(b"Status: OK");
let frame = TerminalFrame::new(80, 24, &data);
let result = match_status_message(&frame, "Status: OK");
assert!(result.is_ok());
}
#[test]
fn match_not_visible_returns_failure_when_present() {
let frame = TerminalFrame::new(80, 24, b"Hello World");
let failure = match_not_visible(&frame, "Hello").expect_err("should be Err");
assert!(matches!(
&failure.expected,
crate::assertion::Expected::NotVisible { needle, .. } if needle == "Hello"
));
}
#[test]
fn match_selected_tab_inspects_header_occurrence_when_label_repeats_in_body() {
let mut data: Vec<u8> = Vec::new();
data.extend_from_slice(b"\x1b[1mProjects\x1b[0m Sessions");
for _ in 0..6 {
data.extend_from_slice(b"\r\n");
}
data.extend_from_slice(b"Projects mentioned here is plain body text");
let frame = TerminalFrame::new(80, 24, &data);
let result = match_selected_tab(&frame, "Projects");
assert!(result.is_ok());
}
#[test]
fn match_unselected_tab_inspects_header_occurrence_when_label_repeats_in_body() {
let mut data: Vec<u8> = Vec::new();
data.extend_from_slice(b"Projects Sessions");
for _ in 0..6 {
data.extend_from_slice(b"\r\n");
}
data.extend_from_slice(b"\x1b[1mSessions\x1b[0m heading");
let frame = TerminalFrame::new(80, 24, &data);
let result = match_unselected_tab(&frame, "Sessions");
assert!(result.is_ok());
}
#[test]
fn match_selected_tab_failure_is_scoped_to_header_region() {
let mut data: Vec<u8> = Vec::new();
data.extend_from_slice(b"Projects Sessions");
for _ in 0..6 {
data.extend_from_slice(b"\r\n");
}
data.extend_from_slice(b"\x1b[1mProjects\x1b[0m elsewhere");
let frame = TerminalFrame::new(80, 24, &data);
let failure = match_selected_tab(&frame, "Projects").expect_err("should be Err");
let header = Region::top_row(frame.cols());
assert_eq!(failure.region, Some(header));
assert_eq!(failure.matched_spans.len(), 1);
assert_eq!(failure.matched_spans[0].rect.row, 0);
}
#[test]
fn match_unselected_tab_failure_is_scoped_to_header_region() {
let mut data: Vec<u8> = Vec::new();
data.extend_from_slice(b"Projects \x1b[1mSessions\x1b[0m");
for _ in 0..6 {
data.extend_from_slice(b"\r\n");
}
data.extend_from_slice(b"Sessions elsewhere");
let frame = TerminalFrame::new(80, 24, &data);
let failure = match_unselected_tab(&frame, "Sessions").expect_err("should be Err");
let header = Region::top_row(frame.cols());
assert_eq!(failure.region, Some(header));
assert_eq!(failure.matched_spans.len(), 1);
assert_eq!(failure.matched_spans[0].rect.row, 0);
}
#[test]
fn match_recipes_compose_with_soft_assertions() {
let frame = TerminalFrame::new(80, 24, b"Other Header");
let mut soft = crate::assertion::SoftAssertions::new();
soft.check(match_selected_tab(&frame, "Projects"));
soft.check(match_keybinding_hint(&frame, "Quit"));
let failures = soft.into_failures();
assert_eq!(failures.len(), 2);
}
}