use archlens_domain::{ CodeElement, CodeElementKind, CodeGraph, DiagramLevel, FilePath, ModuleName, Relationship, RelationshipKind, Visibility, ports::DiagramRenderer, }; use archlens_mermaid::MermaidRenderer; fn build_type_level_graph() -> CodeGraph { let mut graph = CodeGraph::new(); graph.add_element( CodeElement::new( "OrderService", CodeElementKind::Class, FilePath::new("src/service.rs").unwrap(), 1, ) .unwrap(), ); graph.add_element( CodeElement::new( "Order", CodeElementKind::Struct, FilePath::new("src/order.rs").unwrap(), 1, ) .unwrap(), ); graph.add_relationship( Relationship::new("OrderService", "Order", RelationshipKind::Composition).unwrap(), ); graph } #[test] fn renders_class_diagram_with_elements_and_composition() { let renderer = MermaidRenderer::new(); let output = renderer.render(&build_type_level_graph()).unwrap(); let content = output.files()[0].content(); assert!(content.contains("classDiagram")); assert!(content.contains("class OrderService")); assert!(content.contains("class Order")); assert!(content.contains("OrderService --> Order")); } #[test] fn inheritance_uses_different_arrow() { let mut graph = CodeGraph::new(); graph.add_element( CodeElement::new( "Animal", CodeElementKind::Class, FilePath::new("src/animal.rs").unwrap(), 1, ) .unwrap(), ); graph.add_element( CodeElement::new( "Dog", CodeElementKind::Class, FilePath::new("src/dog.rs").unwrap(), 1, ) .unwrap(), ); graph.add_relationship( Relationship::new("Dog", "Animal", RelationshipKind::Inheritance).unwrap(), ); let renderer = MermaidRenderer::new(); let output = renderer.render(&graph).unwrap(); let content = output.files()[0].content(); assert!(content.contains("Dog <|-- Animal")); } #[test] fn elements_show_kind_and_generics() { let mut graph = CodeGraph::new(); graph.add_element( CodeElement::new( "Repository", CodeElementKind::Trait, FilePath::new("src/repo.rs").unwrap(), 1, ) .unwrap() .with_generics(vec!["T".to_string()]), ); let renderer = MermaidRenderer::new(); let output = renderer.render(&graph).unwrap(); let content = output.files()[0].content(); assert!(content.contains("class Repository~T~")); } #[test] fn private_elements_show_visibility_annotation() { let mut graph = CodeGraph::new(); graph.add_element( CodeElement::new( "InternalHelper", CodeElementKind::Class, FilePath::new("src/helper.rs").unwrap(), 1, ) .unwrap() .with_visibility(Visibility::Private), ); let renderer = MermaidRenderer::new(); let output = renderer.render(&graph).unwrap(); let content = output.files()[0].content(); assert!(content.contains("class InternalHelper")); assert!(content.contains("<>")); } #[test] fn renders_module_level_flowchart() { let mut graph = CodeGraph::new(); graph.add_element( CodeElement::new( "OrderService", CodeElementKind::Class, FilePath::new("src/orders/service.rs").unwrap(), 1, ) .unwrap() .with_module(ModuleName::new("Orders").unwrap()), ); graph.add_element( CodeElement::new( "BillingService", CodeElementKind::Class, FilePath::new("src/billing/service.rs").unwrap(), 1, ) .unwrap() .with_module(ModuleName::new("Billing").unwrap()), ); graph.add_relationship( Relationship::new( "OrderService", "BillingService", RelationshipKind::Composition, ) .unwrap(), ); let renderer = MermaidRenderer::with_level(DiagramLevel::Module); let output = renderer.render(&graph).unwrap(); let content = output.files()[0].content(); assert!(content.contains("graph TD")); assert!(content.contains("Orders")); assert!(content.contains("Billing")); assert!(content.contains("Orders --> Billing")); } #[test] fn empty_graph_produces_valid_diagram() { let renderer = MermaidRenderer::new(); let output = renderer.render(&CodeGraph::new()).unwrap(); let content = output.files()[0].content(); assert!(content.contains("classDiagram")); } #[test] fn project_level_renders_subgraphs_for_grouped_projects() { let mut graph = CodeGraph::new(); graph.add_element( CodeElement::new( "myapp-domain", CodeElementKind::Project, FilePath::new("crates/domain/Cargo.toml").unwrap(), 1, ) .unwrap(), ); graph.add_element( CodeElement::new( "myapp-sqlite", CodeElementKind::Project, FilePath::new("crates/adapters/sqlite/Cargo.toml").unwrap(), 1, ) .unwrap() .with_module(ModuleName::new("Adapters").unwrap()), ); graph.add_element( CodeElement::new( "myapp-nats", CodeElementKind::Project, FilePath::new("crates/adapters/nats/Cargo.toml").unwrap(), 1, ) .unwrap() .with_module(ModuleName::new("Adapters").unwrap()), ); graph.add_relationship( Relationship::new( "myapp-sqlite", "myapp-domain", RelationshipKind::Composition, ) .unwrap(), ); let renderer = MermaidRenderer::with_level(DiagramLevel::Project); let output = renderer.render(&graph).unwrap(); let content = output.files()[0].content(); assert!(content.contains("graph TD")); assert!(content.contains("subgraph Adapters")); assert!(content.contains("myapp-sqlite")); assert!(content.contains("myapp-nats")); assert!(content.contains("myapp-domain")); assert!(content.contains("-->")); } #[test] fn type_level_groups_elements_by_module() { let mut graph = CodeGraph::new(); graph.add_element( CodeElement::new( "OrderService", CodeElementKind::Class, FilePath::new("src/orders/service.rs").unwrap(), 1, ) .unwrap() .with_module(ModuleName::new("Orders").unwrap()), ); graph.add_element( CodeElement::new( "Order", CodeElementKind::Struct, FilePath::new("src/orders/order.rs").unwrap(), 1, ) .unwrap() .with_module(ModuleName::new("Orders").unwrap()), ); graph.add_element( CodeElement::new( "Invoice", CodeElementKind::Struct, FilePath::new("src/billing/invoice.rs").unwrap(), 1, ) .unwrap() .with_module(ModuleName::new("Billing").unwrap()), ); let renderer = MermaidRenderer::new(); let output = renderer.render(&graph).unwrap(); let content = output.files()[0].content(); assert!(content.contains("namespace Orders")); assert!(content.contains("namespace Billing")); assert!(content.contains("OrderService")); assert!(content.contains("Invoice")); } #[test] fn module_level_aggregates_cross_module_deps_into_single_arrow() { let mut graph = CodeGraph::new(); graph.add_element( CodeElement::new( "OrderService", CodeElementKind::Class, FilePath::new("src/orders/service.rs").unwrap(), 1, ) .unwrap() .with_module(ModuleName::new("Orders").unwrap()), ); graph.add_element( CodeElement::new( "OrderRepo", CodeElementKind::Trait, FilePath::new("src/orders/repo.rs").unwrap(), 1, ) .unwrap() .with_module(ModuleName::new("Orders").unwrap()), ); graph.add_element( CodeElement::new( "DbPool", CodeElementKind::Struct, FilePath::new("src/infra/db.rs").unwrap(), 1, ) .unwrap() .with_module(ModuleName::new("Infra").unwrap()), ); graph.add_element( CodeElement::new( "SqliteRepo", CodeElementKind::Struct, FilePath::new("src/infra/sqlite.rs").unwrap(), 1, ) .unwrap() .with_module(ModuleName::new("Infra").unwrap()), ); // Two types in Orders depend on two types in Infra graph.add_relationship( Relationship::new("OrderService", "DbPool", RelationshipKind::Composition).unwrap(), ); graph.add_relationship( Relationship::new("OrderRepo", "SqliteRepo", RelationshipKind::Composition).unwrap(), ); let renderer = MermaidRenderer::with_level(DiagramLevel::Module); let output = renderer.render(&graph).unwrap(); let content = output.files()[0].content(); let arrow_count = content.matches("Orders --> Infra").count(); assert_eq!( arrow_count, 1, "should have exactly one aggregated arrow, got:\n{content}" ); }