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