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