use text_document::{
BlockFormat, FlowElement, FragmentContent, ListStyle, MoveMode, TextBlock, TextDocument,
TextFormat,
};
fn new_doc_with_text(text: &str) -> TextDocument {
let doc = TextDocument::new();
doc.set_plain_text(text).unwrap();
doc
}
fn first_block(doc: &TextDocument) -> TextBlock {
match &doc.flow()[0] {
FlowElement::Block(b) => b.clone(),
_ => panic!("expected block"),
}
}
#[test]
fn text_returns_block_content() {
let doc = new_doc_with_text("Hello world");
let block = first_block(&doc);
assert_eq!(block.text(), "Hello world");
}
#[test]
fn text_empty_doc_block() {
let doc = TextDocument::new();
let block = first_block(&doc);
assert_eq!(block.text(), "");
}
#[test]
fn length_returns_char_count() {
let doc = new_doc_with_text("Hello");
let block = first_block(&doc);
assert_eq!(block.length(), 5);
}
#[test]
fn length_unicode() {
let doc = new_doc_with_text("café");
let block = first_block(&doc);
assert_eq!(block.length(), 4);
}
#[test]
fn is_empty_on_empty_block() {
let doc = TextDocument::new();
let block = first_block(&doc);
assert!(block.is_empty());
}
#[test]
fn is_empty_on_non_empty_block() {
let doc = new_doc_with_text("Hi");
let block = first_block(&doc);
assert!(!block.is_empty());
}
#[test]
fn is_valid_for_existing_block() {
let doc = new_doc_with_text("Hello");
let block = first_block(&doc);
assert!(block.is_valid());
}
#[test]
fn block_by_id_none_for_bogus_id() {
let doc = new_doc_with_text("Hello");
assert!(doc.block_by_id(999999).is_none());
}
#[test]
fn is_valid_false_after_block_deleted() {
let doc = new_doc_with_text("First\nSecond");
let block1 = doc.block_by_number(1).unwrap();
assert!(block1.is_valid());
doc.set_plain_text("Only one block").unwrap();
assert!(
!block1.is_valid(),
"block handle should be invalid after document reset"
);
}
#[test]
fn id_is_stable() {
let doc = new_doc_with_text("Hello");
let block = first_block(&doc);
let id1 = block.id();
let id2 = block.id();
assert_eq!(id1, id2);
}
#[test]
fn position_first_block_is_zero() {
let doc = new_doc_with_text("Hello\nWorld");
let block = first_block(&doc);
assert_eq!(block.position(), 0);
}
#[test]
fn position_second_block() {
let doc = new_doc_with_text("Hello\nWorld");
let block = doc.block_by_number(1).unwrap();
assert_eq!(block.position(), 6);
}
#[test]
fn block_number_first() {
let doc = new_doc_with_text("A\nB\nC");
let block = first_block(&doc);
assert_eq!(block.block_number(), 0);
}
#[test]
fn block_number_second() {
let doc = new_doc_with_text("A\nB\nC");
let block = doc.block_by_number(1).unwrap();
assert_eq!(block.block_number(), 1);
}
#[test]
fn block_number_third() {
let doc = new_doc_with_text("A\nB\nC");
let block = doc.block_by_number(2).unwrap();
assert_eq!(block.block_number(), 2);
}
#[test]
fn next_from_first_block() {
let doc = new_doc_with_text("A\nB\nC");
let block = doc.block_by_number(0).unwrap();
let next = block.next().unwrap();
assert_eq!(next.text(), "B");
}
#[test]
fn next_from_middle_block() {
let doc = new_doc_with_text("A\nB\nC");
let block = doc.block_by_number(1).unwrap();
let next = block.next().unwrap();
assert_eq!(next.text(), "C");
}
#[test]
fn next_from_last_block_is_none() {
let doc = new_doc_with_text("A\nB\nC");
let block = doc.block_by_number(2).unwrap();
assert!(block.next().is_none());
}
#[test]
fn previous_from_last_block() {
let doc = new_doc_with_text("A\nB\nC");
let block = doc.block_by_number(2).unwrap();
let prev = block.previous().unwrap();
assert_eq!(prev.text(), "B");
}
#[test]
fn previous_from_middle_block() {
let doc = new_doc_with_text("A\nB\nC");
let block = doc.block_by_number(1).unwrap();
let prev = block.previous().unwrap();
assert_eq!(prev.text(), "A");
}
#[test]
fn previous_from_first_block_is_none() {
let doc = new_doc_with_text("A\nB\nC");
let block = doc.block_by_number(0).unwrap();
assert!(block.previous().is_none());
}
#[test]
fn next_and_previous_none_for_single_block() {
let doc = new_doc_with_text("Only one block");
let block = first_block(&doc);
assert!(block.next().is_none());
assert!(block.previous().is_none());
}
#[test]
fn next_previous_roundtrip() {
let doc = new_doc_with_text("A\nB\nC");
let block = doc.block_by_number(1).unwrap();
let next = block.next().unwrap();
let back = next.previous().unwrap();
assert_eq!(back.id(), block.id());
}
#[test]
fn frame_returns_valid_handle() {
let doc = new_doc_with_text("Hello");
let block = first_block(&doc);
let frame = block.frame();
assert!(frame.id() > 0);
}
#[test]
fn table_cell_returns_none_for_regular_block() {
let doc = new_doc_with_text("Hello");
let block = first_block(&doc);
assert!(block.table_cell().is_none());
}
#[test]
fn block_format_default() {
let doc = new_doc_with_text("Hello");
let block = first_block(&doc);
let fmt = block.block_format();
assert_eq!(fmt.alignment, None);
}
#[test]
fn block_format_after_set() {
let doc = new_doc_with_text("Hello");
let cursor = doc.cursor_at(0);
cursor.set_position(5, MoveMode::KeepAnchor);
let fmt = BlockFormat {
alignment: Some(text_document::Alignment::Center),
..Default::default()
};
cursor.set_block_format(&fmt).unwrap();
let block = first_block(&doc);
assert_eq!(
block.block_format().alignment,
Some(text_document::Alignment::Center)
);
}
#[test]
fn char_format_at_default() {
let doc = new_doc_with_text("Hello");
let block = first_block(&doc);
let fmt = block.char_format_at(0);
assert!(fmt.is_some());
}
#[test]
fn char_format_at_with_bold() {
let doc = new_doc_with_text("Hello world");
let cursor = doc.cursor_at(0);
cursor.set_position(5, MoveMode::KeepAnchor);
let fmt = TextFormat {
font_bold: Some(true),
..Default::default()
};
cursor.set_char_format(&fmt).unwrap();
let block = first_block(&doc);
let fmt_at_0 = block.char_format_at(0).unwrap();
assert_eq!(fmt_at_0.font_bold, Some(true));
let fmt_at_6 = block
.char_format_at(6)
.expect("position 6 is within 'Hello world'");
assert_ne!(fmt_at_6.font_bold, Some(true));
}
#[test]
fn char_format_at_out_of_range() {
let doc = new_doc_with_text("Hello");
let block = first_block(&doc);
assert!(block.char_format_at(999).is_none());
}
#[test]
fn fragments_single_run() {
let doc = new_doc_with_text("Hello");
let block = first_block(&doc);
let frags = block.fragments();
assert!(!frags.is_empty());
if let FragmentContent::Text {
text,
offset,
length,
..
} = &frags[0]
{
assert_eq!(text, "Hello");
assert_eq!(*offset, 0);
assert_eq!(*length, 5);
} else {
panic!("expected Text fragment");
}
}
#[test]
fn fragments_unicode_offsets() {
let doc = new_doc_with_text("café");
let block = first_block(&doc);
let frags = block.fragments();
assert!(!frags.is_empty());
if let FragmentContent::Text {
text,
offset,
length,
..
} = &frags[0]
{
assert_eq!(text, "café");
assert_eq!(*offset, 0);
assert_eq!(*length, 4); } else {
panic!("expected Text fragment");
}
}
#[test]
fn fragments_multiple_runs_after_formatting() {
let doc = new_doc_with_text("Hello world");
let cursor = doc.cursor_at(0);
cursor.set_position(5, MoveMode::KeepAnchor);
let fmt = TextFormat {
font_bold: Some(true),
..Default::default()
};
cursor.set_char_format(&fmt).unwrap();
let block = first_block(&doc);
let frags = block.fragments();
assert!(
frags.len() >= 2,
"expected at least 2 fragments, got {}",
frags.len()
);
}
#[test]
fn fragments_offsets_are_sequential() {
let doc = new_doc_with_text("Hello world");
let cursor = doc.cursor_at(0);
cursor.set_position(5, MoveMode::KeepAnchor);
let fmt = TextFormat {
font_bold: Some(true),
..Default::default()
};
cursor.set_char_format(&fmt).unwrap();
let block = first_block(&doc);
let frags = block.fragments();
let mut expected_offset = 0;
for frag in &frags {
match frag {
FragmentContent::Text { offset, length, .. } => {
assert_eq!(*offset, expected_offset);
expected_offset += length;
}
FragmentContent::Image { offset, .. } => {
assert_eq!(*offset, expected_offset);
expected_offset += 1;
}
}
}
}
#[test]
fn list_returns_none_for_non_list_block() {
let doc = new_doc_with_text("Hello");
let block = first_block(&doc);
assert!(block.list().is_none());
}
#[test]
fn list_item_index_returns_none_for_non_list_block() {
let doc = new_doc_with_text("Hello");
let block = first_block(&doc);
assert!(block.list_item_index().is_none());
}
#[test]
fn list_returns_handle_after_create_list() {
let doc = new_doc_with_text("Item one\nItem two");
let cursor = doc.cursor();
cursor.set_position(0, MoveMode::MoveAnchor);
cursor.set_position(17, MoveMode::KeepAnchor); cursor.create_list(ListStyle::Decimal).unwrap();
let block = first_block(&doc);
let list = block.list();
assert!(list.is_some(), "block should belong to a list");
}
#[test]
fn list_item_index_after_create_list() {
let doc = new_doc_with_text("Item one\nItem two");
let cursor = doc.cursor();
cursor.set_position(0, MoveMode::MoveAnchor);
cursor.set_position(17, MoveMode::KeepAnchor);
cursor.create_list(ListStyle::Decimal).unwrap();
let block0 = doc.block_by_number(0).unwrap();
assert_eq!(block0.list_item_index(), Some(0));
let block1 = doc.block_by_number(1).unwrap();
assert_eq!(block1.list_item_index(), Some(1));
}
#[test]
fn snapshot_captures_all_fields() {
let doc = new_doc_with_text("Hello world");
let block = first_block(&doc);
let snap = block.snapshot();
assert_eq!(snap.block_id, block.id());
assert_eq!(snap.text, "Hello world");
assert_eq!(snap.position, 0);
assert_eq!(snap.length, 11);
assert!(!snap.fragments.is_empty());
assert!(snap.list_info.is_none());
}
#[test]
fn snapshot_captures_list_info() {
let doc = new_doc_with_text("Item one\nItem two");
let cursor = doc.cursor();
cursor.set_position(0, MoveMode::MoveAnchor);
cursor.set_position(17, MoveMode::KeepAnchor);
cursor.create_list(ListStyle::Disc).unwrap();
let block = doc.block_by_number(0).unwrap();
let snap = block.snapshot();
assert!(snap.list_info.is_some());
let info = snap.list_info.unwrap();
assert_eq!(info.style, ListStyle::Disc);
assert_eq!(info.item_index, 0);
assert!(!info.marker.is_empty()); }
#[test]
fn snapshot_second_list_item() {
let doc = new_doc_with_text("One\nTwo\nThree");
let cursor = doc.cursor();
cursor.set_position(0, MoveMode::MoveAnchor);
cursor.set_position(13, MoveMode::KeepAnchor);
cursor.create_list(ListStyle::Decimal).unwrap();
let block2 = doc.block_by_number(2).unwrap();
let snap = block2.snapshot();
let info = snap.list_info.unwrap();
assert_eq!(info.item_index, 2);
assert!(
info.marker.contains('3'),
"marker should contain '3', got: {}",
info.marker
);
}
#[test]
fn snapshot_block_format() {
let doc = new_doc_with_text("Hello");
let cursor = doc.cursor_at(0);
let fmt = BlockFormat {
heading_level: Some(2),
..Default::default()
};
cursor.set_block_format(&fmt).unwrap();
let block = first_block(&doc);
let snap = block.snapshot();
assert_eq!(snap.block_format.heading_level, Some(2));
}
#[test]
fn snapshot_has_parent_frame_id() {
let doc = new_doc_with_text("Hello");
let block = first_block(&doc);
let snap = block.snapshot();
assert!(
snap.parent_frame_id.is_some(),
"snapshot should have parent_frame_id"
);
assert_eq!(snap.parent_frame_id.unwrap(), block.frame().id());
}
#[test]
fn snapshot_non_table_block_has_no_table_cell() {
let doc = new_doc_with_text("Hello");
let block = first_block(&doc);
let snap = block.snapshot();
assert!(
snap.table_cell.is_none(),
"non-table block should have no table_cell context"
);
}
#[test]
fn snapshot_table_cell_block_has_table_cell_context() {
let doc = new_doc_with_text("Before");
let cursor = doc.cursor_at(6);
cursor.insert_table(2, 2).unwrap();
let flow = doc.flow();
let table = flow
.iter()
.find_map(|e| match e {
FlowElement::Table(t) => Some(t.clone()),
_ => None,
})
.expect("should have a table");
let cell = table.cell(0, 0).expect("cell (0,0) should exist");
let cell_blocks = cell.blocks();
assert!(
!cell_blocks.is_empty(),
"cell should have at least one block"
);
let snap = cell_blocks[0].snapshot();
assert!(
snap.table_cell.is_some(),
"block inside table cell should have table_cell context"
);
let ctx = snap.table_cell.unwrap();
assert_eq!(ctx.table_id, table.id());
assert_eq!(ctx.row, 0);
assert_eq!(ctx.column, 0);
}
#[test]
fn text_block_is_clone() {
let doc = new_doc_with_text("Hello");
let block = first_block(&doc);
let cloned = block.clone();
assert_eq!(block.id(), cloned.id());
assert_eq!(block.text(), cloned.text());
}