use crate::channels::telegram::flow_chrome::{GoalSection, ProseSection};
use crate::channels::telegram::plan_card::{render_plan_card_html, render_plan_card_rich_html};
const TITLE: &str = "Merge PR #123 (schema heal) + fix on top + tests";
fn rows() -> Vec<String> {
vec![
"☑ First task".to_string(),
"☑ Second task".to_string(),
"☐ Third task".to_string(),
]
}
fn rich_has_block_break_between(html: &str, first: &str, second: &str) -> bool {
let Some(a) = html.find(first) else {
return false;
};
let Some(b) = html.find(second) else {
return false;
};
if b <= a {
return false;
}
let between = &html[a + first.len()..b];
between.contains("</p>") || between.contains("<br") || between.contains("</details>")
}
#[test]
fn rich_checklist_rows_are_separated_by_block_level_markup() {
let html = render_plan_card_rich_html(Some(TITLE), Some(&rows()), None, None)
.expect("a card with a title and checklist must render");
assert!(
rich_has_block_break_between(&html, "First task", "Second task"),
"checklist rows must be separated by block-level markup in the rich \
dialect, otherwise they collapse into one line. Got:\n{html}"
);
assert!(
rich_has_block_break_between(&html, "Second task", "Third task"),
"every consecutive pair must break, not just the first. Got:\n{html}"
);
assert!(
rich_has_block_break_between(&html, "</b>", "First task"),
"the title must break before the first checklist row. Got:\n{html}"
);
}
#[test]
fn rich_card_never_relies_on_a_bare_newline_to_break_a_line() {
let prose = vec![ProseSection {
heading: Some("Context".to_string()),
body: "Some prose paragraph.".to_string(),
}];
let goal = GoalSection {
text: "Ship it".to_string(),
completed: false,
};
let html = render_plan_card_rich_html(Some(TITLE), Some(&rows()), Some(&prose), Some(&goal))
.expect("full card must render");
assert!(
rich_has_block_break_between(&html, "</b>", "Some prose paragraph"),
"title → prose seam must be a real break. Got:\n{html}"
);
assert!(
rich_has_block_break_between(&html, "Some prose paragraph.", "First task"),
"prose → checklist seam must be a real break. Got:\n{html}"
);
assert!(
rich_has_block_break_between(&html, "Third task", "goal"),
"checklist → goal seam must be a real break. Got:\n{html}"
);
}
#[test]
fn rich_card_does_not_wrap_block_level_elements_in_a_paragraph() {
let prose = vec![ProseSection {
heading: Some("Context".to_string()),
body: "Some prose.".to_string(),
}];
let html = render_plan_card_rich_html(Some(TITLE), None, Some(&prose), None)
.expect("card with prose must render");
assert!(
!html.contains("<p><details"),
"a collapsible must not be wrapped in a paragraph. Got:\n{html}"
);
assert!(
!html.contains("<p><blockquote"),
"a blockquote must not be wrapped in a paragraph. Got:\n{html}"
);
}
#[test]
fn classic_card_still_breaks_lines_with_newlines() {
let html = render_plan_card_html(Some(TITLE), Some(&rows()), None, None)
.expect("a card with a title and checklist must render");
assert!(
html.contains("☑ First task\n☑ Second task\n☐ Third task"),
"classic rows must stay newline-separated. Got:\n{html}"
);
assert!(
!html.contains("<p>"),
"classic ParseMode::Html has no <p> tag — emitting one shows it as \
literal text or gets the message rejected. Got:\n{html}"
);
}
#[test]
fn classic_card_keeps_the_blank_line_before_the_goal() {
let goal = GoalSection {
text: "Ship it".to_string(),
completed: false,
};
let html = render_plan_card_html(Some(TITLE), Some(&rows()), None, Some(&goal))
.expect("card with a goal must render");
assert!(
html.contains("☐ Third task\n\n<blockquote"),
"a blank line must separate the checklist from the goal. Got:\n{html}"
);
}
#[test]
fn classic_card_has_no_gap_before_the_goal_when_there_is_no_body() {
let goal = GoalSection {
text: "Ship it".to_string(),
completed: false,
};
let html = render_plan_card_html(Some(TITLE), None, None, Some(&goal))
.expect("title + goal must render");
assert!(
!html.contains("\n\n"),
"no body means no gap before the goal. Got:\n{html}"
);
}
#[test]
fn an_empty_card_still_renders_nothing() {
assert!(render_plan_card_rich_html(None, None, None, None).is_none());
assert!(render_plan_card_html(None, None, None, None).is_none());
assert!(
render_plan_card_rich_html(Some(" "), None, None, None).is_none(),
"a whitespace-only title is not content"
);
}