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 direct_access::block::block_controller;
25pub use direct_access::block::dtos::CreateBlockDto;
26pub use direct_access::document::document_controller;
27pub use direct_access::document::dtos::CreateDocumentDto;
28pub use direct_access::frame::frame_controller;
29pub use direct_access::frame::dtos::CreateFrameDto;
30pub use direct_access::inline_element::dtos::CreateInlineElementDto;
31pub use direct_access::inline_element::inline_element_controller;
32pub use direct_access::root::dtos::CreateRootDto;
33pub use direct_access::root::root_controller;
34
35/// Create an in-memory database with a Root and empty Document.
36///
37/// Returns `(DbContext, Arc<EventHub>, UndoRedoManager)`.
38pub fn setup() -> Result<(DbContext, Arc<EventHub>, UndoRedoManager)> {
39    let db_context = DbContext::new()?;
40    let event_hub = Arc::new(EventHub::new());
41    let mut undo_redo_manager = UndoRedoManager::new();
42
43    let root =
44        root_controller::create_orphan(&db_context, &event_hub, &CreateRootDto::default())?;
45
46    let _doc = document_controller::create(
47        &db_context,
48        &event_hub,
49        &mut undo_redo_manager,
50        None,
51        &CreateDocumentDto::default(),
52        root.id,
53        -1,
54    )?;
55
56    Ok((db_context, event_hub, undo_redo_manager))
57}
58
59/// Create an in-memory database with a Root, Document, and imported text content.
60///
61/// Splits the text on `\n` and creates one Block + InlineElement per line,
62/// mirroring what `document_io::import_plain_text` does but without depending
63/// on the `document_io` crate.
64///
65/// Returns `(DbContext, Arc<EventHub>, UndoRedoManager)`.
66pub fn setup_with_text(text: &str) -> Result<(DbContext, Arc<EventHub>, UndoRedoManager)> {
67    let (db_context, event_hub, mut undo_redo_manager) = setup()?;
68
69    // Get Root -> Document -> existing Frame
70    let root_rels =
71        root_controller::get_relationship(&db_context, &1, &RootRelationshipField::Document)?;
72    let doc_id = root_rels[0];
73    let frame_ids = document_controller::get_relationship(
74        &db_context,
75        &doc_id,
76        &DocumentRelationshipField::Frames,
77    )?;
78
79    // Remove existing frames (the setup creates one empty frame)
80    for fid in &frame_ids {
81        frame_controller::remove(&db_context, &event_hub, &mut undo_redo_manager, None, fid)?;
82    }
83
84    // Create a fresh frame
85    let frame = frame_controller::create(
86        &db_context,
87        &event_hub,
88        &mut undo_redo_manager,
89        None,
90        &CreateFrameDto::default(),
91        doc_id,
92        -1,
93    )?;
94
95    // Split text into lines and create blocks
96    let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
97    let lines: Vec<&str> = normalized.split('\n').collect();
98    let mut document_position: i64 = 0;
99    let mut total_chars: i64 = 0;
100
101    for (i, line) in lines.iter().enumerate() {
102        let line_len = line.chars().count() as i64;
103
104        let block_dto = CreateBlockDto {
105            plain_text: line.to_string(),
106            text_length: line_len,
107            document_position,
108            ..Default::default()
109        };
110
111        let block = block_controller::create(
112            &db_context,
113            &event_hub,
114            &mut undo_redo_manager,
115            None,
116            &block_dto,
117            frame.id,
118            i as i32,
119        )?;
120
121        let elem_dto = CreateInlineElementDto {
122            content: InlineContent::Text(line.to_string()),
123            ..Default::default()
124        };
125
126        inline_element_controller::create(
127            &db_context,
128            &event_hub,
129            &mut undo_redo_manager,
130            None,
131            &elem_dto,
132            block.id,
133            0,
134        )?;
135
136        total_chars += line_len;
137        document_position += line_len;
138        if i < lines.len() - 1 {
139            document_position += 1; // block separator
140        }
141    }
142
143    // Update document cached fields
144    let mut doc = document_controller::get(&db_context, &doc_id)?
145        .ok_or_else(|| anyhow::anyhow!("Document not found"))?;
146    doc.character_count = total_chars;
147    doc.block_count = lines.len() as i64;
148    document_controller::update(&db_context, &event_hub, &mut undo_redo_manager, None, &doc.into())?;
149
150    // Clear undo history so test starts clean
151    undo_redo_manager.clear_all_stacks();
152
153    Ok((db_context, event_hub, undo_redo_manager))
154}
155
156/// Export the current document as plain text by reading blocks and
157/// concatenating their `plain_text` fields with `\n` separators.
158pub fn export_text(db_context: &DbContext, _event_hub: &Arc<EventHub>) -> Result<String> {
159    let block_ids = get_block_ids(db_context)?;
160    let mut blocks = Vec::new();
161    for id in &block_ids {
162        if let Some(b) = block_controller::get(db_context, id)? {
163            blocks.push(b);
164        }
165    }
166    blocks.sort_by_key(|b| b.document_position);
167    let text = blocks
168        .iter()
169        .map(|b| b.plain_text.as_str())
170        .collect::<Vec<&str>>()
171        .join("\n");
172    Ok(text)
173}
174
175/// Get the first frame's block IDs.
176pub fn get_block_ids(db_context: &DbContext) -> Result<Vec<EntityId>> {
177    let root_rels =
178        root_controller::get_relationship(db_context, &1, &RootRelationshipField::Document)?;
179    let doc_id = root_rels[0];
180    let frame_ids = document_controller::get_relationship(
181        db_context,
182        &doc_id,
183        &DocumentRelationshipField::Frames,
184    )?;
185    let frame_id = frame_ids[0];
186    frame_controller::get_relationship(db_context, &frame_id, &FrameRelationshipField::Blocks)
187}
188
189/// Get the element IDs for a given block.
190pub fn get_element_ids(db_context: &DbContext, block_id: &EntityId) -> Result<Vec<EntityId>> {
191    block_controller::get_relationship(db_context, block_id, &BlockRelationshipField::Elements)
192}
193
194/// Get the first block's element IDs.
195pub fn get_first_block_element_ids(db_context: &DbContext) -> Result<Vec<EntityId>> {
196    let block_ids = get_block_ids(db_context)?;
197    get_element_ids(db_context, &block_ids[0])
198}
199
200/// Get the first frame ID for the document.
201pub fn get_frame_id(db_context: &DbContext) -> Result<EntityId> {
202    let root_rels =
203        root_controller::get_relationship(db_context, &1, &RootRelationshipField::Document)?;
204    let doc_id = root_rels[0];
205    let frame_ids = document_controller::get_relationship(
206        db_context,
207        &doc_id,
208        &DocumentRelationshipField::Frames,
209    )?;
210    Ok(frame_ids[0])
211}
212
213/// Basic document statistics retrieved directly from entity data.
214pub struct BasicStats {
215    pub character_count: i64,
216    pub block_count: i64,
217    pub frame_count: i64,
218}
219
220/// Get basic document statistics by reading the Document entity directly.
221pub fn get_document_stats(db_context: &DbContext) -> Result<BasicStats> {
222    let root_rels =
223        root_controller::get_relationship(db_context, &1, &RootRelationshipField::Document)?;
224    let doc_id = root_rels[0];
225    let doc = document_controller::get(db_context, &doc_id)?
226        .ok_or_else(|| anyhow::anyhow!("Document not found"))?;
227    let frame_ids = document_controller::get_relationship(
228        db_context,
229        &doc_id,
230        &DocumentRelationshipField::Frames,
231    )?;
232    Ok(BasicStats {
233        character_count: doc.character_count,
234        block_count: doc.block_count,
235        frame_count: frame_ids.len() as i64,
236    })
237}