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::{BlockRelationshipDto, CreateBlockDto, UpdateBlockDto};
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, UpdateInlineElementDto};
33pub use direct_access::inline_element::inline_element_controller;
34pub use direct_access::list::dtos::CreateListDto as CreateListEntityDto;
35pub use direct_access::list::list_controller;
36pub use direct_access::root::dtos::CreateRootDto;
37pub use direct_access::root::root_controller;
38pub use direct_access::table::dtos::{CreateTableDto, TableDto};
39pub use direct_access::table::table_controller;
40pub use direct_access::table_cell::dtos::{CreateTableCellDto, TableCellDto};
41pub use direct_access::table_cell::table_cell_controller;
42
43/// Create an in-memory database with a Root and empty Document.
44///
45/// Returns `(DbContext, Arc<EventHub>, UndoRedoManager)`.
46pub fn setup() -> Result<(DbContext, Arc<EventHub>, UndoRedoManager)> {
47    let db_context = DbContext::new()?;
48    let event_hub = Arc::new(EventHub::new());
49    let mut undo_redo_manager = UndoRedoManager::new();
50
51    let root = root_controller::create_orphan(&db_context, &event_hub, &CreateRootDto::default())?;
52
53    let _doc = document_controller::create(
54        &db_context,
55        &event_hub,
56        &mut undo_redo_manager,
57        None,
58        &CreateDocumentDto::default(),
59        root.id,
60        -1,
61    )?;
62
63    Ok((db_context, event_hub, undo_redo_manager))
64}
65
66/// Create an in-memory database with a Root, Document, and imported text content.
67///
68/// Splits the text on `\n` and creates one Block + InlineElement per line,
69/// mirroring what `document_io::import_plain_text` does but without depending
70/// on the `document_io` crate.
71///
72/// Returns `(DbContext, Arc<EventHub>, UndoRedoManager)`.
73pub fn setup_with_text(text: &str) -> Result<(DbContext, Arc<EventHub>, UndoRedoManager)> {
74    let (db_context, event_hub, mut undo_redo_manager) = setup()?;
75
76    // Get Root -> Document -> existing Frame
77    let root_rels =
78        root_controller::get_relationship(&db_context, &1, &RootRelationshipField::Document)?;
79    let doc_id = root_rels[0];
80    let frame_ids = document_controller::get_relationship(
81        &db_context,
82        &doc_id,
83        &DocumentRelationshipField::Frames,
84    )?;
85
86    // Remove existing frames (the setup creates one empty frame)
87    for fid in &frame_ids {
88        frame_controller::remove(&db_context, &event_hub, &mut undo_redo_manager, None, fid)?;
89    }
90
91    // Create a fresh frame
92    let frame = frame_controller::create(
93        &db_context,
94        &event_hub,
95        &mut undo_redo_manager,
96        None,
97        &CreateFrameDto::default(),
98        doc_id,
99        -1,
100    )?;
101
102    // Split text into lines and create blocks
103    let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
104    let lines: Vec<&str> = normalized.split('\n').collect();
105    let mut document_position: i64 = 0;
106    let mut total_chars: i64 = 0;
107    let mut child_order: Vec<i64> = Vec::new();
108
109    for (i, line) in lines.iter().enumerate() {
110        let line_len = line.chars().count() as i64;
111
112        let block_dto = CreateBlockDto {
113            plain_text: line.to_string(),
114            text_length: line_len,
115            document_position,
116            ..Default::default()
117        };
118
119        let block = block_controller::create(
120            &db_context,
121            &event_hub,
122            &mut undo_redo_manager,
123            None,
124            &block_dto,
125            frame.id,
126            i as i32,
127        )?;
128
129        child_order.push(block.id as i64);
130
131        let elem_dto = CreateInlineElementDto {
132            content: InlineContent::Text(line.to_string()),
133            ..Default::default()
134        };
135
136        inline_element_controller::create(
137            &db_context,
138            &event_hub,
139            &mut undo_redo_manager,
140            None,
141            &elem_dto,
142            block.id,
143            0,
144        )?;
145
146        total_chars += line_len;
147        document_position += line_len;
148        if i < lines.len() - 1 {
149            document_position += 1; // block separator
150        }
151    }
152
153    // Update frame child_order to include all blocks
154    let mut updated_frame = frame_controller::get(&db_context, &frame.id)?
155        .ok_or_else(|| anyhow::anyhow!("Frame not found"))?;
156    updated_frame.child_order = child_order;
157    frame_controller::update(
158        &db_context,
159        &event_hub,
160        &mut undo_redo_manager,
161        None,
162        &updated_frame.into(),
163    )?;
164
165    // Update document cached fields
166    let mut doc = document_controller::get(&db_context, &doc_id)?
167        .ok_or_else(|| anyhow::anyhow!("Document not found"))?;
168    doc.character_count = total_chars;
169    doc.block_count = lines.len() as i64;
170    document_controller::update(
171        &db_context,
172        &event_hub,
173        &mut undo_redo_manager,
174        None,
175        &doc.into(),
176    )?;
177
178    // Clear undo history so test starts clean
179    undo_redo_manager.clear_all_stacks();
180
181    Ok((db_context, event_hub, undo_redo_manager))
182}
183
184/// Export the current document as plain text by reading blocks and
185/// concatenating their `plain_text` fields with `\n` separators.
186pub fn export_text(db_context: &DbContext, _event_hub: &Arc<EventHub>) -> Result<String> {
187    let block_ids = get_block_ids(db_context)?;
188    let mut blocks = Vec::new();
189    for id in &block_ids {
190        if let Some(b) = block_controller::get(db_context, id)? {
191            blocks.push(b);
192        }
193    }
194    blocks.sort_by_key(|b| b.document_position);
195    let text = blocks
196        .iter()
197        .map(|b| b.plain_text.as_str())
198        .collect::<Vec<&str>>()
199        .join("\n");
200    Ok(text)
201}
202
203/// Get the first frame's block IDs.
204pub fn get_block_ids(db_context: &DbContext) -> Result<Vec<EntityId>> {
205    let root_rels =
206        root_controller::get_relationship(db_context, &1, &RootRelationshipField::Document)?;
207    let doc_id = root_rels[0];
208    let frame_ids = document_controller::get_relationship(
209        db_context,
210        &doc_id,
211        &DocumentRelationshipField::Frames,
212    )?;
213    let frame_id = frame_ids[0];
214    frame_controller::get_relationship(db_context, &frame_id, &FrameRelationshipField::Blocks)
215}
216
217/// Get the element IDs for a given block.
218pub fn get_element_ids(db_context: &DbContext, block_id: &EntityId) -> Result<Vec<EntityId>> {
219    block_controller::get_relationship(db_context, block_id, &BlockRelationshipField::Elements)
220}
221
222/// Get the first block's element IDs.
223pub fn get_first_block_element_ids(db_context: &DbContext) -> Result<Vec<EntityId>> {
224    let block_ids = get_block_ids(db_context)?;
225    get_element_ids(db_context, &block_ids[0])
226}
227
228/// Get the first frame ID for the document.
229pub fn get_frame_id(db_context: &DbContext) -> Result<EntityId> {
230    let root_rels =
231        root_controller::get_relationship(db_context, &1, &RootRelationshipField::Document)?;
232    let doc_id = root_rels[0];
233    let frame_ids = document_controller::get_relationship(
234        db_context,
235        &doc_id,
236        &DocumentRelationshipField::Frames,
237    )?;
238    Ok(frame_ids[0])
239}
240
241/// Get all table IDs in the document.
242pub fn get_table_ids(db_context: &DbContext) -> Result<Vec<EntityId>> {
243    let root_rels =
244        root_controller::get_relationship(db_context, &1, &RootRelationshipField::Document)?;
245    let doc_id = root_rels[0];
246    document_controller::get_relationship(db_context, &doc_id, &DocumentRelationshipField::Tables)
247}
248
249/// Get all cell IDs for a given table.
250pub fn get_table_cell_ids(db_context: &DbContext, table_id: &EntityId) -> Result<Vec<EntityId>> {
251    table_controller::get_relationship(db_context, table_id, &TableRelationshipField::Cells)
252}
253
254/// Get all cells for a table, sorted by row then column.
255pub fn get_sorted_cells(db_context: &DbContext, table_id: &EntityId) -> Result<Vec<TableCellDto>> {
256    let cell_ids = get_table_cell_ids(db_context, table_id)?;
257    let cells_opt = table_cell_controller::get_multi(db_context, &cell_ids)?;
258    let mut cells: Vec<TableCellDto> = cells_opt.into_iter().flatten().collect();
259    cells.sort_by(|a, b| a.row.cmp(&b.row).then(a.column.cmp(&b.column)));
260    Ok(cells)
261}
262
263/// Get all block IDs across all frames in the document (not just the first frame).
264pub fn get_all_block_ids(db_context: &DbContext) -> Result<Vec<EntityId>> {
265    let root_rels =
266        root_controller::get_relationship(db_context, &1, &RootRelationshipField::Document)?;
267    let doc_id = root_rels[0];
268    let frame_ids = document_controller::get_relationship(
269        db_context,
270        &doc_id,
271        &DocumentRelationshipField::Frames,
272    )?;
273    let mut all_block_ids = Vec::new();
274    for fid in &frame_ids {
275        let block_ids =
276            frame_controller::get_relationship(db_context, fid, &FrameRelationshipField::Blocks)?;
277        all_block_ids.extend(block_ids);
278    }
279    Ok(all_block_ids)
280}
281
282/// Basic document statistics retrieved directly from entity data.
283pub struct BasicStats {
284    pub character_count: i64,
285    pub block_count: i64,
286    pub frame_count: i64,
287}
288
289/// Get basic document statistics by reading the Document entity directly.
290pub fn get_document_stats(db_context: &DbContext) -> Result<BasicStats> {
291    let root_rels =
292        root_controller::get_relationship(db_context, &1, &RootRelationshipField::Document)?;
293    let doc_id = root_rels[0];
294    let doc = document_controller::get(db_context, &doc_id)?
295        .ok_or_else(|| anyhow::anyhow!("Document not found"))?;
296    let frame_ids = document_controller::get_relationship(
297        db_context,
298        &doc_id,
299        &DocumentRelationshipField::Frames,
300    )?;
301    Ok(BasicStats {
302        character_count: doc.character_count,
303        block_count: doc.block_count,
304        frame_count: frame_ids.len() as i64,
305    })
306}
307
308// ═══════════════════════════════════════════════════════════════════════════
309// Test-only helpers that build richer documents (tables, lists, images,
310// frames) using entity controllers directly — no feature-crate dependency.
311// ═══════════════════════════════════════════════════════════════════════════
312
313pub struct InsertTableResult {
314    pub table_id: EntityId,
315}
316
317/// Insert a `rows x columns` table at `position` using entity controllers.
318///
319/// Creates the Table, one Frame+Block+EmptyElement per cell, and adjusts
320/// `document_position` for all subsequent blocks.
321pub fn insert_table(
322    db_context: &DbContext,
323    event_hub: &Arc<EventHub>,
324    undo_redo_manager: &mut UndoRedoManager,
325    position: i64,
326    rows: i64,
327    columns: i64,
328) -> Result<InsertTableResult> {
329    let doc_id = get_doc_id(db_context)?;
330
331    // Create table owned by document
332    let table = table_controller::create(
333        db_context,
334        event_hub,
335        undo_redo_manager,
336        None,
337        &CreateTableDto {
338            rows,
339            columns,
340            ..Default::default()
341        },
342        doc_id,
343        -1,
344    )?;
345
346    let table_size = rows * columns;
347    let mut cell_blocks: Vec<EntityId> = Vec::new();
348
349    for r in 0..rows {
350        for c in 0..columns {
351            // Create cell frame owned by document
352            let cell_frame = frame_controller::create(
353                db_context,
354                event_hub,
355                undo_redo_manager,
356                None,
357                &CreateFrameDto::default(),
358                doc_id,
359                -1,
360            )?;
361
362            // Create block in cell frame
363            let block = block_controller::create(
364                db_context,
365                event_hub,
366                undo_redo_manager,
367                None,
368                &CreateBlockDto::default(),
369                cell_frame.id,
370                0,
371            )?;
372
373            // Create empty element in block
374            inline_element_controller::create(
375                db_context,
376                event_hub,
377                undo_redo_manager,
378                None,
379                &CreateInlineElementDto {
380                    content: InlineContent::Empty,
381                    ..Default::default()
382                },
383                block.id,
384                0,
385            )?;
386
387            // Create table cell owned by table
388            table_cell_controller::create(
389                db_context,
390                event_hub,
391                undo_redo_manager,
392                None,
393                &CreateTableCellDto {
394                    row: r,
395                    column: c,
396                    row_span: 1,
397                    column_span: 1,
398                    cell_frame: Some(cell_frame.id),
399                    ..Default::default()
400                },
401                table.id,
402                -1,
403            )?;
404
405            cell_blocks.push(block.id);
406        }
407    }
408
409    // Assign document_positions to cell blocks
410    let mut current_pos = position;
411    for &bid in &cell_blocks {
412        let mut b = block_controller::get(db_context, &bid)?
413            .ok_or_else(|| anyhow::anyhow!("Block not found"))?;
414        b.document_position = current_pos;
415        block_controller::update(db_context, event_hub, undo_redo_manager, None, &b.into())?;
416        current_pos += 1;
417    }
418
419    // Shift existing blocks (not cell blocks) that are at or after position
420    let all_bids = get_all_block_ids(db_context)?;
421    for bid in &all_bids {
422        if cell_blocks.contains(bid) {
423            continue;
424        }
425        let b = block_controller::get(db_context, bid)?
426            .ok_or_else(|| anyhow::anyhow!("Block not found"))?;
427        if b.document_position >= position {
428            let mut updated = b.clone();
429            updated.document_position += table_size;
430            block_controller::update(
431                db_context,
432                event_hub,
433                undo_redo_manager,
434                None,
435                &updated.into(),
436            )?;
437        }
438    }
439
440    undo_redo_manager.clear_all_stacks();
441    Ok(InsertTableResult { table_id: table.id })
442}
443
444pub struct CreateListResult {
445    pub list_id: EntityId,
446}
447
448/// Create a list spanning blocks in `[position, anchor]` using entity controllers.
449pub fn create_list(
450    db_context: &DbContext,
451    event_hub: &Arc<EventHub>,
452    undo_redo_manager: &mut UndoRedoManager,
453    position: i64,
454    anchor: i64,
455    style: common::entities::ListStyle,
456) -> Result<CreateListResult> {
457    let doc_id = get_doc_id(db_context)?;
458    let sel_start = std::cmp::min(position, anchor);
459    let sel_end = std::cmp::max(position, anchor);
460
461    let list = list_controller::create(
462        db_context,
463        event_hub,
464        undo_redo_manager,
465        None,
466        &CreateListEntityDto {
467            style,
468            ..Default::default()
469        },
470        doc_id,
471        -1,
472    )?;
473
474    // Find overlapping blocks and assign them to the list
475    let all_bids = get_all_block_ids(db_context)?;
476    for bid in &all_bids {
477        let b = block_controller::get(db_context, bid)?
478            .ok_or_else(|| anyhow::anyhow!("Block not found"))?;
479        let block_start = b.document_position;
480        let block_end = block_start + b.text_length;
481        if block_end >= sel_start && block_start <= sel_end {
482            block_controller::set_relationship(
483                db_context,
484                event_hub,
485                undo_redo_manager,
486                None,
487                &BlockRelationshipDto {
488                    id: b.id,
489                    field: BlockRelationshipField::List,
490                    right_ids: vec![list.id],
491                },
492            )?;
493        }
494    }
495
496    undo_redo_manager.clear_all_stacks();
497    Ok(CreateListResult { list_id: list.id })
498}
499
500pub struct InsertImageResult {
501    pub new_position: i64,
502    pub element_id: EntityId,
503}
504
505/// Insert an image inline element at `position` using entity controllers.
506///
507/// Splits the text element at the insertion offset when needed.
508pub fn insert_image(
509    db_context: &DbContext,
510    event_hub: &Arc<EventHub>,
511    undo_redo_manager: &mut UndoRedoManager,
512    position: i64,
513    image_name: &str,
514    width: i64,
515    height: i64,
516) -> Result<InsertImageResult> {
517    // Find block containing position
518    let all_bids = get_all_block_ids(db_context)?;
519    let mut blocks = Vec::new();
520    for bid in &all_bids {
521        blocks.push(
522            block_controller::get(db_context, bid)?
523                .ok_or_else(|| anyhow::anyhow!("Block not found"))?,
524        );
525    }
526    blocks.sort_by_key(|b| b.document_position);
527
528    let (target_block, offset) = blocks
529        .iter()
530        .find_map(|b| {
531            let s = b.document_position;
532            let e = s + b.text_length;
533            if position >= s && position <= e {
534                Some((b.clone(), (position - s) as usize))
535            } else {
536                None
537            }
538        })
539        .ok_or_else(|| anyhow::anyhow!("No block at position {}", position))?;
540
541    // Walk elements to find the one at offset
542    let elem_ids = block_controller::get_relationship(
543        db_context,
544        &target_block.id,
545        &BlockRelationshipField::Elements,
546    )?;
547
548    let mut running = 0usize;
549    let mut insert_after_idx: i32 = -1;
550    for (idx, eid) in elem_ids.iter().enumerate() {
551        let elem = inline_element_controller::get(db_context, eid)?
552            .ok_or_else(|| anyhow::anyhow!("Element not found"))?;
553        let elen = match &elem.content {
554            InlineContent::Text(s) => s.chars().count(),
555            InlineContent::Image { .. } => 1,
556            InlineContent::Empty => 0,
557        };
558
559        if running + elen > offset && offset > running {
560            // Split text element
561            if let InlineContent::Text(ref text) = elem.content {
562                let chars: Vec<char> = text.chars().collect();
563                let local = offset - running;
564                let before: String = chars[..local].iter().collect();
565                let after: String = chars[local..].iter().collect();
566
567                // Shrink original to 'before'
568                let mut upd: UpdateInlineElementDto = elem.clone().into();
569                upd.content = InlineContent::Text(before);
570                inline_element_controller::update(
571                    db_context,
572                    event_hub,
573                    undo_redo_manager,
574                    None,
575                    &upd,
576                )?;
577
578                // Create 'after' element
579                let after_entity: common::entities::InlineElement = elem.clone().into();
580                let mut after_create = CreateInlineElementDto::from(after_entity);
581                after_create.content = InlineContent::Text(after);
582                inline_element_controller::create(
583                    db_context,
584                    event_hub,
585                    undo_redo_manager,
586                    None,
587                    &after_create,
588                    target_block.id,
589                    (idx as i32) + 1,
590                )?;
591            }
592            insert_after_idx = (idx as i32) + 1;
593            break;
594        }
595        running += elen;
596        if running >= offset {
597            insert_after_idx = (idx as i32) + 1;
598            break;
599        }
600    }
601    if insert_after_idx < 0 {
602        insert_after_idx = elem_ids.len() as i32;
603    }
604
605    // Create image element
606    let img = inline_element_controller::create(
607        db_context,
608        event_hub,
609        undo_redo_manager,
610        None,
611        &CreateInlineElementDto {
612            content: InlineContent::Image {
613                name: image_name.to_string(),
614                width,
615                height,
616                quality: 100,
617            },
618            ..Default::default()
619        },
620        target_block.id,
621        insert_after_idx,
622    )?;
623
624    // Update block text_length (+1) and shift subsequent blocks
625    let mut upd_block = target_block.clone();
626    upd_block.text_length += 1;
627    block_controller::update(
628        db_context,
629        event_hub,
630        undo_redo_manager,
631        None,
632        &upd_block.into(),
633    )?;
634
635    for b in &blocks {
636        if b.id != target_block.id && b.document_position > target_block.document_position {
637            let mut shifted = b.clone();
638            shifted.document_position += 1;
639            block_controller::update(
640                db_context,
641                event_hub,
642                undo_redo_manager,
643                None,
644                &shifted.into(),
645            )?;
646        }
647    }
648
649    undo_redo_manager.clear_all_stacks();
650    Ok(InsertImageResult {
651        new_position: position + 1,
652        element_id: img.id,
653    })
654}
655
656pub struct InsertFrameResult {
657    pub frame_id: EntityId,
658}
659
660/// Insert a sub-frame at `position` using entity controllers.
661///
662/// The new frame contains one empty block and is registered in
663/// the document's frames collection.
664pub fn insert_frame(
665    db_context: &DbContext,
666    event_hub: &Arc<EventHub>,
667    undo_redo_manager: &mut UndoRedoManager,
668    position: i64,
669) -> Result<InsertFrameResult> {
670    let doc_id = get_doc_id(db_context)?;
671
672    let new_frame = frame_controller::create(
673        db_context,
674        event_hub,
675        undo_redo_manager,
676        None,
677        &CreateFrameDto::default(),
678        doc_id,
679        -1,
680    )?;
681
682    let block = block_controller::create(
683        db_context,
684        event_hub,
685        undo_redo_manager,
686        None,
687        &CreateBlockDto {
688            document_position: position,
689            ..Default::default()
690        },
691        new_frame.id,
692        0,
693    )?;
694
695    inline_element_controller::create(
696        db_context,
697        event_hub,
698        undo_redo_manager,
699        None,
700        &CreateInlineElementDto {
701            content: InlineContent::Empty,
702            ..Default::default()
703        },
704        block.id,
705        0,
706    )?;
707
708    undo_redo_manager.clear_all_stacks();
709    Ok(InsertFrameResult {
710        frame_id: new_frame.id,
711    })
712}
713
714fn get_doc_id(db_context: &DbContext) -> Result<EntityId> {
715    let root_rels =
716        root_controller::get_relationship(db_context, &1, &RootRelationshipField::Document)?;
717    Ok(root_rels[0])
718}