Skip to main content

text_document_test_harness/
lib.rs

1//! Shared test setup utilities for text-document crate tests.
2//!
3//! Provides helpers to create an in-memory document with content,
4//! export text, and traverse the entity tree. This crate depends only
5//! on `common` and `direct_access` — it reimplements plain-text import
6//! and export directly via entity controllers, so it does **not** depend
7//! on `document_io` or any feature crate, breaking the circular
8//! dev-dependency chain.
9
10use anyhow::Result;
11use common::database::db_context::DbContext;
12use common::entities::InlineContent;
13use common::event::EventHub;
14use common::types::EntityId;
15use common::undo_redo::UndoRedoManager;
16use std::sync::Arc;
17
18// Re-export commonly used types and controllers for convenience
19pub use common::direct_access::block::block_repository::BlockRelationshipField;
20pub use common::direct_access::document::document_repository::DocumentRelationshipField;
21pub use common::direct_access::frame::frame_repository::FrameRelationshipField;
22pub use common::direct_access::root::root_repository::RootRelationshipField;
23
24pub use common::direct_access::table::table_repository::TableRelationshipField;
25pub use common::direct_access::table_cell::table_cell_repository::TableCellRelationshipField;
26pub use direct_access::block::block_controller;
27pub use direct_access::block::dtos::CreateBlockDto;
28pub use direct_access::document::document_controller;
29pub use direct_access::document::dtos::CreateDocumentDto;
30pub use direct_access::frame::dtos::CreateFrameDto;
31pub use direct_access::frame::frame_controller;
32pub use direct_access::inline_element::dtos::CreateInlineElementDto;
33pub use direct_access::inline_element::inline_element_controller;
34pub use direct_access::root::dtos::CreateRootDto;
35pub use direct_access::root::root_controller;
36pub use direct_access::table::dtos::TableDto;
37pub use direct_access::table::table_controller;
38pub use direct_access::table_cell::dtos::TableCellDto;
39pub use direct_access::table_cell::table_cell_controller;
40
41/// Create an in-memory database with a Root and empty Document.
42///
43/// Returns `(DbContext, Arc<EventHub>, UndoRedoManager)`.
44pub fn setup() -> Result<(DbContext, Arc<EventHub>, UndoRedoManager)> {
45    let db_context = DbContext::new()?;
46    let event_hub = Arc::new(EventHub::new());
47    let mut undo_redo_manager = UndoRedoManager::new();
48
49    let root = root_controller::create_orphan(&db_context, &event_hub, &CreateRootDto::default())?;
50
51    let _doc = document_controller::create(
52        &db_context,
53        &event_hub,
54        &mut undo_redo_manager,
55        None,
56        &CreateDocumentDto::default(),
57        root.id,
58        -1,
59    )?;
60
61    Ok((db_context, event_hub, undo_redo_manager))
62}
63
64/// Create an in-memory database with a Root, Document, and imported text content.
65///
66/// Splits the text on `\n` and creates one Block + InlineElement per line,
67/// mirroring what `document_io::import_plain_text` does but without depending
68/// on the `document_io` crate.
69///
70/// Returns `(DbContext, Arc<EventHub>, UndoRedoManager)`.
71pub fn setup_with_text(text: &str) -> Result<(DbContext, Arc<EventHub>, UndoRedoManager)> {
72    let (db_context, event_hub, mut undo_redo_manager) = setup()?;
73
74    // Get Root -> Document -> existing Frame
75    let root_rels =
76        root_controller::get_relationship(&db_context, &1, &RootRelationshipField::Document)?;
77    let doc_id = root_rels[0];
78    let frame_ids = document_controller::get_relationship(
79        &db_context,
80        &doc_id,
81        &DocumentRelationshipField::Frames,
82    )?;
83
84    // Remove existing frames (the setup creates one empty frame)
85    for fid in &frame_ids {
86        frame_controller::remove(&db_context, &event_hub, &mut undo_redo_manager, None, fid)?;
87    }
88
89    // Create a fresh frame
90    let frame = frame_controller::create(
91        &db_context,
92        &event_hub,
93        &mut undo_redo_manager,
94        None,
95        &CreateFrameDto::default(),
96        doc_id,
97        -1,
98    )?;
99
100    // Split text into lines and create blocks
101    let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
102    let lines: Vec<&str> = normalized.split('\n').collect();
103    let mut document_position: i64 = 0;
104    let mut total_chars: i64 = 0;
105
106    for (i, line) in lines.iter().enumerate() {
107        let line_len = line.chars().count() as i64;
108
109        let block_dto = CreateBlockDto {
110            plain_text: line.to_string(),
111            text_length: line_len,
112            document_position,
113            ..Default::default()
114        };
115
116        let block = block_controller::create(
117            &db_context,
118            &event_hub,
119            &mut undo_redo_manager,
120            None,
121            &block_dto,
122            frame.id,
123            i as i32,
124        )?;
125
126        let elem_dto = CreateInlineElementDto {
127            content: InlineContent::Text(line.to_string()),
128            ..Default::default()
129        };
130
131        inline_element_controller::create(
132            &db_context,
133            &event_hub,
134            &mut undo_redo_manager,
135            None,
136            &elem_dto,
137            block.id,
138            0,
139        )?;
140
141        total_chars += line_len;
142        document_position += line_len;
143        if i < lines.len() - 1 {
144            document_position += 1; // block separator
145        }
146    }
147
148    // Update document cached fields
149    let mut doc = document_controller::get(&db_context, &doc_id)?
150        .ok_or_else(|| anyhow::anyhow!("Document not found"))?;
151    doc.character_count = total_chars;
152    doc.block_count = lines.len() as i64;
153    document_controller::update(
154        &db_context,
155        &event_hub,
156        &mut undo_redo_manager,
157        None,
158        &doc.into(),
159    )?;
160
161    // Clear undo history so test starts clean
162    undo_redo_manager.clear_all_stacks();
163
164    Ok((db_context, event_hub, undo_redo_manager))
165}
166
167/// Export the current document as plain text by reading blocks and
168/// concatenating their `plain_text` fields with `\n` separators.
169pub fn export_text(db_context: &DbContext, _event_hub: &Arc<EventHub>) -> Result<String> {
170    let block_ids = get_block_ids(db_context)?;
171    let mut blocks = Vec::new();
172    for id in &block_ids {
173        if let Some(b) = block_controller::get(db_context, id)? {
174            blocks.push(b);
175        }
176    }
177    blocks.sort_by_key(|b| b.document_position);
178    let text = blocks
179        .iter()
180        .map(|b| b.plain_text.as_str())
181        .collect::<Vec<&str>>()
182        .join("\n");
183    Ok(text)
184}
185
186/// Get the first frame's block IDs.
187pub fn get_block_ids(db_context: &DbContext) -> Result<Vec<EntityId>> {
188    let root_rels =
189        root_controller::get_relationship(db_context, &1, &RootRelationshipField::Document)?;
190    let doc_id = root_rels[0];
191    let frame_ids = document_controller::get_relationship(
192        db_context,
193        &doc_id,
194        &DocumentRelationshipField::Frames,
195    )?;
196    let frame_id = frame_ids[0];
197    frame_controller::get_relationship(db_context, &frame_id, &FrameRelationshipField::Blocks)
198}
199
200/// Get the element IDs for a given block.
201pub fn get_element_ids(db_context: &DbContext, block_id: &EntityId) -> Result<Vec<EntityId>> {
202    block_controller::get_relationship(db_context, block_id, &BlockRelationshipField::Elements)
203}
204
205/// Get the first block's element IDs.
206pub fn get_first_block_element_ids(db_context: &DbContext) -> Result<Vec<EntityId>> {
207    let block_ids = get_block_ids(db_context)?;
208    get_element_ids(db_context, &block_ids[0])
209}
210
211/// Get the first frame ID for the document.
212pub fn get_frame_id(db_context: &DbContext) -> Result<EntityId> {
213    let root_rels =
214        root_controller::get_relationship(db_context, &1, &RootRelationshipField::Document)?;
215    let doc_id = root_rels[0];
216    let frame_ids = document_controller::get_relationship(
217        db_context,
218        &doc_id,
219        &DocumentRelationshipField::Frames,
220    )?;
221    Ok(frame_ids[0])
222}
223
224/// Get all table IDs in the document.
225pub fn get_table_ids(db_context: &DbContext) -> Result<Vec<EntityId>> {
226    let root_rels =
227        root_controller::get_relationship(db_context, &1, &RootRelationshipField::Document)?;
228    let doc_id = root_rels[0];
229    document_controller::get_relationship(db_context, &doc_id, &DocumentRelationshipField::Tables)
230}
231
232/// Get all cell IDs for a given table.
233pub fn get_table_cell_ids(db_context: &DbContext, table_id: &EntityId) -> Result<Vec<EntityId>> {
234    table_controller::get_relationship(db_context, table_id, &TableRelationshipField::Cells)
235}
236
237/// Get all cells for a table, sorted by row then column.
238pub fn get_sorted_cells(db_context: &DbContext, table_id: &EntityId) -> Result<Vec<TableCellDto>> {
239    let cell_ids = get_table_cell_ids(db_context, table_id)?;
240    let cells_opt = table_cell_controller::get_multi(db_context, &cell_ids)?;
241    let mut cells: Vec<TableCellDto> = cells_opt.into_iter().flatten().collect();
242    cells.sort_by(|a, b| a.row.cmp(&b.row).then(a.column.cmp(&b.column)));
243    Ok(cells)
244}
245
246/// Get all block IDs across all frames in the document (not just the first frame).
247pub fn get_all_block_ids(db_context: &DbContext) -> Result<Vec<EntityId>> {
248    let root_rels =
249        root_controller::get_relationship(db_context, &1, &RootRelationshipField::Document)?;
250    let doc_id = root_rels[0];
251    let frame_ids = document_controller::get_relationship(
252        db_context,
253        &doc_id,
254        &DocumentRelationshipField::Frames,
255    )?;
256    let mut all_block_ids = Vec::new();
257    for fid in &frame_ids {
258        let block_ids =
259            frame_controller::get_relationship(db_context, fid, &FrameRelationshipField::Blocks)?;
260        all_block_ids.extend(block_ids);
261    }
262    Ok(all_block_ids)
263}
264
265/// Basic document statistics retrieved directly from entity data.
266pub struct BasicStats {
267    pub character_count: i64,
268    pub block_count: i64,
269    pub frame_count: i64,
270}
271
272/// Get basic document statistics by reading the Document entity directly.
273pub fn get_document_stats(db_context: &DbContext) -> Result<BasicStats> {
274    let root_rels =
275        root_controller::get_relationship(db_context, &1, &RootRelationshipField::Document)?;
276    let doc_id = root_rels[0];
277    let doc = document_controller::get(db_context, &doc_id)?
278        .ok_or_else(|| anyhow::anyhow!("Document not found"))?;
279    let frame_ids = document_controller::get_relationship(
280        db_context,
281        &doc_id,
282        &DocumentRelationshipField::Frames,
283    )?;
284    Ok(BasicStats {
285        character_count: doc.character_count,
286        block_count: doc.block_count,
287        frame_count: frame_ids.len() as i64,
288    })
289}