avalonia-mcp-tools 0.1.0

MCP tools for AvaloniaUI development assistance
Documentation
//! Testing Integration tool - Test setup patterns
use avalonia_mcp_core::error::AvaloniaMcpError;
use avalonia_mcp_core::markdown::MarkdownOutputBuilder;
use rmcp::model::{CallToolResult, Content};
use rmcp::tool;
use serde::{Deserialize, Serialize};
use schemars::JsonSchema;

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TestingIntegrationParams {
    pub test_type: Option<String>,
    pub include_examples: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct UITestParams { pub test_type: Option<String>, pub include_page_objects: Option<bool> }

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MocksAndBuildersParams { pub entity_name: String, pub include_fluent_builder: Option<bool> }

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct PerformanceTestParams { pub test_area: Option<String>, pub include_profiling: Option<bool> }

#[derive(Debug, Clone, Default)]
pub struct TestingIntegrationTool;

impl TestingIntegrationTool {
    pub fn new() -> Self { Self }

    #[tool(description = "Generate test setup patterns for AvaloniaUI applications. Covers unit tests, UI tests with Avalonia.Headless, and mocking strategies.")]
    pub async fn generate_testing_integration(
        &self,
        params: TestingIntegrationParams,
    ) -> Result<CallToolResult, AvaloniaMcpError> {
        let include_examples = params.include_examples.unwrap_or(true);
        let test_type = params.test_type.as_deref().unwrap_or("all");

        let output = match test_type {
            "unit" => self.generate_unit_tests(include_examples),
            "ui" => self.generate_ui_tests(include_examples),
            "integration" => self.generate_integration_tests(include_examples),
            _ => self.generate_all_tests(include_examples),
        };

        Ok(CallToolResult::success(vec![Content::text(output)]))
    }

    fn generate_unit_tests(&self, include_examples: bool) -> String {
        let mut builder = MarkdownOutputBuilder::new()
            .heading(1, "Unit Testing Patterns")
            .paragraph("Unit testing best practices for AvaloniaUI applications.")
            .heading(2, "Test Framework");

        if include_examples {
            builder = builder
                .heading(3, "xUnit Test Setup")
                .code_block("csharp", r#"// Test project setup
// dotnet add package xunit
// dotnet add package xunit.runner.visualstudio
// dotnet add package Moq

public class ViewModelTests
{
    [Fact]
    public void ViewModel_PropertyChange_UpdatesView()
    {
        // Arrange
        var mockService = new Mock<IDataService>();
        mockService.Setup(s => s.GetData()).ReturnsAsync(new Data());
        var vm = new MainViewModel(mockService.Object);
        
        // Act
        vm.LoadCommand.Execute(null);
        
        // Assert
        Assert.NotNull(vm.Data);
        mockService.Verify(s => s.GetData(), Times.Once);
    }
    
    [Fact]
    public async Task ViewModel_ErrorHandling_ShowsMessage()
    {
        // Arrange
        var mockService = new Mock<IDataService>();
        mockService.Setup(s => s.GetData()).ThrowsAsync(new Exception("Test"));
        var vm = new MainViewModel(mockService.Object);
        
        // Act
        await vm.LoadCommand.ExecuteAsync(null);
        
        // Assert
        Assert.NotNull(vm.ErrorMessage);
    }
}"#)
                .heading(3, "Mocking Services")
                .code_block("csharp", r#"// Using Moq for mocking
var mockRepo = new Mock<IRepository>();
mockRepo.Setup(r => r.GetById(It.IsAny<int>())).ReturnsAsync(new Entity());
mockRepo.Setup(r => r.Save(It.IsAny<Entity>())).Returns(Task.CompletedTask);

// Using NSubstitute (alternative)
var mockRepo = Substitute.For<IRepository>();
mockRepo.GetById(1).Returns(new Entity());
await mockRepo.Save(Arg.Any<Entity>()).Returns(Task.CompletedTask);"#);
        }

        builder.heading(2, "Best Practices")
            .task_list(vec![(true, "Test one thing per test"), (true, "Use descriptive names"), (true, "Arrange-Act-Assert pattern"), (true, "Mock external dependencies"), (false, "Aim for 80%+ coverage")])
            .build()
    }

    fn generate_ui_tests(&self, include_examples: bool) -> String {
        let mut builder = MarkdownOutputBuilder::new()
            .heading(1, "UI Testing Patterns")
            .paragraph("UI testing with Avalonia.Headless for automated UI tests.")
            .heading(2, "Headless Testing");

        if include_examples {
            builder = builder
                .heading(3, "Avalonia.Headless Setup")
                .code_block("csharp", r#"// Test project: dotnet add package Avalonia.Headless.XUnit

public class MainWindowTests : IClassFixture<AppFixture>
{
    private readonly AppFixture _fixture;
    
    public MainWindowTests(AppFixture fixture) => _fixture = fixture;
    
    [Fact]
    public async Task Button_Click_UpdatesCounter()
    {
        // Arrange
        var window = new MainWindow();
        window.Show(_fixture.App);
        
        // Act
        var button = window.FindControl<Button>("IncrementButton");
        button.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));
        await _fixture.App.Dispatcher.UIThread.RunJobs();
        
        // Assert
        var textBlock = window.FindControl<TextBlock>("CounterText");
        Assert.Equal("1", textBlock.Text);
    }
    
    [Fact]
    public async Task TextBox_Input_Validates()
    {
        // Arrange
        var window = new MainWindow();
        window.Show(_fixture.App);
        
        // Act
        var textBox = window.FindControl<TextBox>("NameInput");
        textBox.Text = "Test";
        textBox.RaiseEvent(new RoutedEventArgs(TextInputEvent));
        
        // Assert
        Assert.False(textBox.Classes.Contains("error"));
    }
}"#);
        }

        builder.heading(2, "UI Test Best Practices")
            .task_list(vec![(true, "Use Page Object pattern"), (true, "Wait for async operations"), (true, "Test user workflows"), (true, "Verify visual states"), (false, "Run in CI pipeline")])
            .build()
    }

    fn generate_integration_tests(&self, include_examples: bool) -> String {
        let mut builder = MarkdownOutputBuilder::new()
            .heading(1, "Integration Testing Patterns")
            .paragraph("Integration testing for full application workflows.")
            .heading(2, "Test Setup");

        if include_examples {
            builder = builder
                .heading(3, "Integration Test Base")
                .code_block("csharp", r#"public class IntegrationTestBase : IDisposable
{
    protected readonly IHost _host;
    protected readonly IServiceProvider _services;
    
    public IntegrationTestBase()
    {
        _host = Host.CreateDefaultBuilder()
            .ConfigureServices(ConfigureServices)
            .Build();
        _host.Start();
        _services = _host.Services;
    }
    
    protected virtual void ConfigureServices(IServiceCollection services)
    {
        // Register test services
        services.AddSingleton<ITestDatabase, TestDatabase>();
    }
    
    public void Dispose() => _host.Dispose();
}"#);
        }

        builder.heading(2, "Integration Test Checklist")
            .task_list(vec![(true, "Use test database"), (true, "Mock external services"), (true, "Test full workflows"), (true, "Clean up after tests"), (false, "Run in isolated environment")])
            .build()
    }

    fn generate_all_tests(&self, include_examples: bool) -> String {
        MarkdownOutputBuilder::new()
            .heading(1, "Complete Testing Guide")
            .paragraph("Comprehensive testing strategy for AvaloniaUI applications.")
            .heading(2, "Testing Pyramid")
            .list(vec!["Unit Tests (70%)", "Integration Tests (20%)", "UI Tests (10%)"])
            .heading(2, "Unit Tests")
            .paragraph(&self.generate_unit_tests(include_examples))
            .heading(2, "UI Tests")
            .paragraph(&self.generate_ui_tests(include_examples))
            .heading(2, "Integration Tests")
            .paragraph(&self.generate_integration_tests(include_examples))
            .heading(2, "CI/CD Integration")
            .code_block("yaml", r#"# GitHub Actions
name: Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: Run tests
      run: dotnet test --collect:"XPlat Code Coverage"
    - name: Upload coverage
      uses: codecov/codecov-action@v4"#)
            .build()
    }

    #[tool(description = "Creates UI automation tests for AvaloniaUI applications using Avalonia.Headless")]
    pub async fn generate_ui_automation_tests(&self, params: UITestParams) -> Result<CallToolResult, AvaloniaMcpError> {
        let test_type = params.test_type.as_deref().unwrap_or("button_click").to_lowercase();
        let include_po = params.include_page_objects.unwrap_or(true);
        let po = if include_po { "\n## Page Objects\n```csharp\npublic class LoginPageObject\n{\n    private readonly IControl _root;\n    public LoginPageObject(IControl root) => _root = root;\n    public IControl UsernameInput => _root.FindControl<TextBox>(\"UsernameBox\");\n    public IControl LoginButton => _root.FindControl<Button>(\"LoginBtn\");\n    public void Login(string user, string pass)\n    {\n        UsernameInput.Text = user;\n        LoginButton.Click();\n    }\n}\n```" } else { "" };
        let builder = MarkdownOutputBuilder::new()
            .heading(1, "UI Automation Tests")
            .heading(2, "Configuration").task_list(vec![(true, format!("Type: {}", test_type)), (true, format!("Page Objects: {}", include_po))])
            .heading(2, "Headless Test").code_block("csharp", "[TestClass]\npublic class UITests\n{\n    [TestMethod]\n    public async Task Button_Click_ShouldUpdateText()\n    {\n        var window = new MainWindow();\n        var button = window.Find<Button>(\"MyButton\");\n        button.Click();\n        Assert.AreEqual(\"Clicked\", window.Find<TextBlock>(\"Status\").Text);\n    }\n}");
        let builder = if include_po { builder.heading(2, "Page Objects").code_block("csharp", &po.replace("\n## Page Objects\n```csharp\n", "").replace("\n```", "")) } else { builder };
        let builder = builder.heading(2, "Setup").list(&["Install Avalonia.Headless.NUnit", "Use BuildMainWindow() for test windows", "Simulate input with Click(), Focus(), Text"]);
        Ok(CallToolResult::success(vec![Content::text(builder.build())]))
    }

    #[tool(description = "Generates mock objects and test data builders for AvaloniaUI testing")]
    pub async fn generate_mocks_and_builders(&self, params: MocksAndBuildersParams) -> Result<CallToolResult, AvaloniaMcpError> {
        if params.entity_name.is_empty() { return Err(AvaloniaMcpError::validation("Entity name cannot be empty")); }
        let include_fluent = params.include_fluent_builder.unwrap_or(true);
        let e = &params.entity_name;
        let builder_code = if include_fluent {
            format!("public class {e}Builder\n{{\n    private int _id = 1;\n    private string _name = \"Test {e}\";\n    public {e}Builder WithId(int id) {{ _id = id; return this; }}\n    public {e}Builder WithName(string name) {{ _name = name; return this; }}\n    public {e} Build() => new() {{ Id = _id, Name = _name }};\n    public static implicit operator {e}({e}Builder b) => b.Build();\n}}")
        } else {
            format!("public static class {e}Factory\n{{\n    public static {e} Create(int id = 1, string name = \"Test\") =>\n        new() {{ Id = id, Name = name }};\n}}")
        };
        let builder = MarkdownOutputBuilder::new()
            .heading(1, &format!("Test Builders: {}", e))
            .heading(2, "Configuration").task_list(vec![(true, format!("Entity: {}", e)), (true, format!("Fluent: {}", include_fluent))])
            .heading(2, "Implementation").code_block("csharp", &builder_code)
            .heading(2, "Usage").code_block("csharp", &format!("var entity = new {e}Builder().WithId(42).WithName(\"Custom\").Build();"))
            .heading(2, "Mock Setup").code_block("csharp", &format!("var mock = new Mock<I{e}Service>();\nmock.Setup(s => s.GetAsync(It.IsAny<int>())).ReturnsAsync(new {e}Builder().Build());"));
        Ok(CallToolResult::success(vec![Content::text(builder.build())]))
    }

    #[tool(description = "Creates performance and load tests for AvaloniaUI applications")]
    pub async fn generate_performance_tests(&self, params: PerformanceTestParams) -> Result<CallToolResult, AvaloniaMcpError> {
        let test_area = params.test_area.as_deref().unwrap_or("rendering").to_lowercase();
        let include_profiling = params.include_profiling.unwrap_or(true);
        let profiling = if include_profiling { "\n## Profiling\n```csharp\npublic static class PerformanceTester\n{\n    public static async Task<long> MeasureAsync(Func<Task> op, int iterations = 100)\n    {\n        var sw = Stopwatch.StartNew();\n        for (int i = 0; i < iterations; i++) await op();\n        sw.Stop();\n        return sw.ElapsedMilliseconds;\n    }\n}\n```" } else { "" };
        let builder = MarkdownOutputBuilder::new()
            .heading(1, "Performance Tests")
            .heading(2, "Configuration").task_list(vec![(true, format!("Area: {}", test_area)), (true, format!("Profiling: {}", include_profiling))])
            .heading(2, "Performance Test").code_block("csharp", "[TestClass]\npublic class PerformanceTests\n{\n    [TestMethod]\n    public async Task Render_With1000Items_ShouldCompleteInTime()\n    {\n        var sw = Stopwatch.StartNew();\n        var listBox = new ListBox { Items = Enumerable.Range(0, 1000) };\n        listBox.Measure(new Size(800, 600));\n        listBox.Arrange(new Rect(0, 0, 800, 600));\n        sw.Stop();\n        Assert.IsTrue(sw.ElapsedMilliseconds < 500);\n    }\n}");
        let builder = if include_profiling { builder.heading(2, "Profiling").code_block("csharp", &profiling.replace("\n## Profiling\n```csharp\n", "").replace("\n```", "")) } else { builder };
        let builder = builder.heading(2, "Metrics").list(&["<500ms for 1000 item rendering", "Memory stable over time", "CPU <10% idle", "60fps animations"]);
        Ok(CallToolResult::success(vec![Content::text(builder.build())]))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    #[tokio::test]
    async fn test_generate_testing() {
        let tool = TestingIntegrationTool::new();
        let result = tool.generate_testing_integration(TestingIntegrationParams { test_type: None, include_examples: Some(true) }).await.unwrap();
        assert!(result.is_error.is_none() || result.is_error == Some(false));
    }

    #[tokio::test]
    async fn test_generate_ui_automation_tests_success() {
        let tool = TestingIntegrationTool::new();
        let params = UITestParams {
            test_type: Some("button_click".to_string()),
            include_page_objects: Some(true),
        };
        let result = tool.generate_ui_automation_tests(params).await.unwrap();
        assert!(result.is_error.is_none() || result.is_error == Some(false));
    }

    #[tokio::test]
    async fn test_generate_mocks_and_builders_success() {
        let tool = TestingIntegrationTool::new();
        let params = MocksAndBuildersParams {
            entity_name: "Order".to_string(),
            include_fluent_builder: Some(true),
        };
        let result = tool.generate_mocks_and_builders(params).await.unwrap();
        assert!(result.is_error.is_none() || result.is_error == Some(false));
    }

    #[tokio::test]
    async fn test_generate_mocks_and_builders_empty_name() {
        let tool = TestingIntegrationTool::new();
        let params = MocksAndBuildersParams {
            entity_name: "".to_string(),
            include_fluent_builder: None,
        };
        let result = tool.generate_mocks_and_builders(params).await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn test_generate_performance_tests_success() {
        let tool = TestingIntegrationTool::new();
        let params = PerformanceTestParams {
            test_area: Some("rendering".to_string()),
            include_profiling: Some(true),
        };
        let result = tool.generate_performance_tests(params).await.unwrap();
        assert!(result.is_error.is_none() || result.is_error == Some(false));
    }
}