1use avalonia_mcp_core::error::AvaloniaMcpError;
3use avalonia_mcp_core::markdown::MarkdownOutputBuilder;
4use rmcp::model::{CallToolResult, Content};
5use rmcp::tool;
6use serde::{Deserialize, Serialize};
7use schemars::JsonSchema;
8
9#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
10pub struct TestingIntegrationParams {
11 pub test_type: Option<String>,
12 pub include_examples: Option<bool>,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
16pub struct UITestParams { pub test_type: Option<String>, pub include_page_objects: Option<bool> }
17
18#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
19pub struct MocksAndBuildersParams { pub entity_name: String, pub include_fluent_builder: Option<bool> }
20
21#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
22pub struct PerformanceTestParams { pub test_area: Option<String>, pub include_profiling: Option<bool> }
23
24#[derive(Debug, Clone, Default)]
25pub struct TestingIntegrationTool;
26
27impl TestingIntegrationTool {
28 pub fn new() -> Self { Self }
29
30 #[tool(description = "Generate test setup patterns for AvaloniaUI applications. Covers unit tests, UI tests with Avalonia.Headless, and mocking strategies.")]
31 pub async fn generate_testing_integration(
32 &self,
33 params: TestingIntegrationParams,
34 ) -> Result<CallToolResult, AvaloniaMcpError> {
35 let include_examples = params.include_examples.unwrap_or(true);
36 let test_type = params.test_type.as_deref().unwrap_or("all");
37
38 let output = match test_type {
39 "unit" => self.generate_unit_tests(include_examples),
40 "ui" => self.generate_ui_tests(include_examples),
41 "integration" => self.generate_integration_tests(include_examples),
42 _ => self.generate_all_tests(include_examples),
43 };
44
45 Ok(CallToolResult::success(vec![Content::text(output)]))
46 }
47
48 fn generate_unit_tests(&self, include_examples: bool) -> String {
49 let mut builder = MarkdownOutputBuilder::new()
50 .heading(1, "Unit Testing Patterns")
51 .paragraph("Unit testing best practices for AvaloniaUI applications.")
52 .heading(2, "Test Framework");
53
54 if include_examples {
55 builder = builder
56 .heading(3, "xUnit Test Setup")
57 .code_block("csharp", r#"// Test project setup
58// dotnet add package xunit
59// dotnet add package xunit.runner.visualstudio
60// dotnet add package Moq
61
62public class ViewModelTests
63{
64 [Fact]
65 public void ViewModel_PropertyChange_UpdatesView()
66 {
67 // Arrange
68 var mockService = new Mock<IDataService>();
69 mockService.Setup(s => s.GetData()).ReturnsAsync(new Data());
70 var vm = new MainViewModel(mockService.Object);
71
72 // Act
73 vm.LoadCommand.Execute(null);
74
75 // Assert
76 Assert.NotNull(vm.Data);
77 mockService.Verify(s => s.GetData(), Times.Once);
78 }
79
80 [Fact]
81 public async Task ViewModel_ErrorHandling_ShowsMessage()
82 {
83 // Arrange
84 var mockService = new Mock<IDataService>();
85 mockService.Setup(s => s.GetData()).ThrowsAsync(new Exception("Test"));
86 var vm = new MainViewModel(mockService.Object);
87
88 // Act
89 await vm.LoadCommand.ExecuteAsync(null);
90
91 // Assert
92 Assert.NotNull(vm.ErrorMessage);
93 }
94}"#)
95 .heading(3, "Mocking Services")
96 .code_block("csharp", r#"// Using Moq for mocking
97var mockRepo = new Mock<IRepository>();
98mockRepo.Setup(r => r.GetById(It.IsAny<int>())).ReturnsAsync(new Entity());
99mockRepo.Setup(r => r.Save(It.IsAny<Entity>())).Returns(Task.CompletedTask);
100
101// Using NSubstitute (alternative)
102var mockRepo = Substitute.For<IRepository>();
103mockRepo.GetById(1).Returns(new Entity());
104await mockRepo.Save(Arg.Any<Entity>()).Returns(Task.CompletedTask);"#);
105 }
106
107 builder.heading(2, "Best Practices")
108 .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")])
109 .build()
110 }
111
112 fn generate_ui_tests(&self, include_examples: bool) -> String {
113 let mut builder = MarkdownOutputBuilder::new()
114 .heading(1, "UI Testing Patterns")
115 .paragraph("UI testing with Avalonia.Headless for automated UI tests.")
116 .heading(2, "Headless Testing");
117
118 if include_examples {
119 builder = builder
120 .heading(3, "Avalonia.Headless Setup")
121 .code_block("csharp", r#"// Test project: dotnet add package Avalonia.Headless.XUnit
122
123public class MainWindowTests : IClassFixture<AppFixture>
124{
125 private readonly AppFixture _fixture;
126
127 public MainWindowTests(AppFixture fixture) => _fixture = fixture;
128
129 [Fact]
130 public async Task Button_Click_UpdatesCounter()
131 {
132 // Arrange
133 var window = new MainWindow();
134 window.Show(_fixture.App);
135
136 // Act
137 var button = window.FindControl<Button>("IncrementButton");
138 button.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));
139 await _fixture.App.Dispatcher.UIThread.RunJobs();
140
141 // Assert
142 var textBlock = window.FindControl<TextBlock>("CounterText");
143 Assert.Equal("1", textBlock.Text);
144 }
145
146 [Fact]
147 public async Task TextBox_Input_Validates()
148 {
149 // Arrange
150 var window = new MainWindow();
151 window.Show(_fixture.App);
152
153 // Act
154 var textBox = window.FindControl<TextBox>("NameInput");
155 textBox.Text = "Test";
156 textBox.RaiseEvent(new RoutedEventArgs(TextInputEvent));
157
158 // Assert
159 Assert.False(textBox.Classes.Contains("error"));
160 }
161}"#);
162 }
163
164 builder.heading(2, "UI Test Best Practices")
165 .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")])
166 .build()
167 }
168
169 fn generate_integration_tests(&self, include_examples: bool) -> String {
170 let mut builder = MarkdownOutputBuilder::new()
171 .heading(1, "Integration Testing Patterns")
172 .paragraph("Integration testing for full application workflows.")
173 .heading(2, "Test Setup");
174
175 if include_examples {
176 builder = builder
177 .heading(3, "Integration Test Base")
178 .code_block("csharp", r#"public class IntegrationTestBase : IDisposable
179{
180 protected readonly IHost _host;
181 protected readonly IServiceProvider _services;
182
183 public IntegrationTestBase()
184 {
185 _host = Host.CreateDefaultBuilder()
186 .ConfigureServices(ConfigureServices)
187 .Build();
188 _host.Start();
189 _services = _host.Services;
190 }
191
192 protected virtual void ConfigureServices(IServiceCollection services)
193 {
194 // Register test services
195 services.AddSingleton<ITestDatabase, TestDatabase>();
196 }
197
198 public void Dispose() => _host.Dispose();
199}"#);
200 }
201
202 builder.heading(2, "Integration Test Checklist")
203 .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")])
204 .build()
205 }
206
207 fn generate_all_tests(&self, include_examples: bool) -> String {
208 MarkdownOutputBuilder::new()
209 .heading(1, "Complete Testing Guide")
210 .paragraph("Comprehensive testing strategy for AvaloniaUI applications.")
211 .heading(2, "Testing Pyramid")
212 .list(vec!["Unit Tests (70%)", "Integration Tests (20%)", "UI Tests (10%)"])
213 .heading(2, "Unit Tests")
214 .paragraph(&self.generate_unit_tests(include_examples))
215 .heading(2, "UI Tests")
216 .paragraph(&self.generate_ui_tests(include_examples))
217 .heading(2, "Integration Tests")
218 .paragraph(&self.generate_integration_tests(include_examples))
219 .heading(2, "CI/CD Integration")
220 .code_block("yaml", r#"# GitHub Actions
221name: Tests
222on: [push, pull_request]
223jobs:
224 test:
225 runs-on: ubuntu-latest
226 steps:
227 - uses: actions/checkout@v4
228 - name: Run tests
229 run: dotnet test --collect:"XPlat Code Coverage"
230 - name: Upload coverage
231 uses: codecov/codecov-action@v4"#)
232 .build()
233 }
234
235 #[tool(description = "Creates UI automation tests for AvaloniaUI applications using Avalonia.Headless")]
236 pub async fn generate_ui_automation_tests(&self, params: UITestParams) -> Result<CallToolResult, AvaloniaMcpError> {
237 let test_type = params.test_type.as_deref().unwrap_or("button_click").to_lowercase();
238 let include_po = params.include_page_objects.unwrap_or(true);
239 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 { "" };
240 let builder = MarkdownOutputBuilder::new()
241 .heading(1, "UI Automation Tests")
242 .heading(2, "Configuration").task_list(vec![(true, format!("Type: {}", test_type)), (true, format!("Page Objects: {}", include_po))])
243 .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}");
244 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 };
245 let builder = builder.heading(2, "Setup").list(&["Install Avalonia.Headless.NUnit", "Use BuildMainWindow() for test windows", "Simulate input with Click(), Focus(), Text"]);
246 Ok(CallToolResult::success(vec![Content::text(builder.build())]))
247 }
248
249 #[tool(description = "Generates mock objects and test data builders for AvaloniaUI testing")]
250 pub async fn generate_mocks_and_builders(&self, params: MocksAndBuildersParams) -> Result<CallToolResult, AvaloniaMcpError> {
251 if params.entity_name.is_empty() { return Err(AvaloniaMcpError::validation("Entity name cannot be empty")); }
252 let include_fluent = params.include_fluent_builder.unwrap_or(true);
253 let e = ¶ms.entity_name;
254 let builder_code = if include_fluent {
255 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}}")
256 } else {
257 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}}")
258 };
259 let builder = MarkdownOutputBuilder::new()
260 .heading(1, &format!("Test Builders: {}", e))
261 .heading(2, "Configuration").task_list(vec![(true, format!("Entity: {}", e)), (true, format!("Fluent: {}", include_fluent))])
262 .heading(2, "Implementation").code_block("csharp", &builder_code)
263 .heading(2, "Usage").code_block("csharp", &format!("var entity = new {e}Builder().WithId(42).WithName(\"Custom\").Build();"))
264 .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());"));
265 Ok(CallToolResult::success(vec![Content::text(builder.build())]))
266 }
267
268 #[tool(description = "Creates performance and load tests for AvaloniaUI applications")]
269 pub async fn generate_performance_tests(&self, params: PerformanceTestParams) -> Result<CallToolResult, AvaloniaMcpError> {
270 let test_area = params.test_area.as_deref().unwrap_or("rendering").to_lowercase();
271 let include_profiling = params.include_profiling.unwrap_or(true);
272 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 { "" };
273 let builder = MarkdownOutputBuilder::new()
274 .heading(1, "Performance Tests")
275 .heading(2, "Configuration").task_list(vec![(true, format!("Area: {}", test_area)), (true, format!("Profiling: {}", include_profiling))])
276 .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}");
277 let builder = if include_profiling { builder.heading(2, "Profiling").code_block("csharp", &profiling.replace("\n## Profiling\n```csharp\n", "").replace("\n```", "")) } else { builder };
278 let builder = builder.heading(2, "Metrics").list(&["<500ms for 1000 item rendering", "Memory stable over time", "CPU <10% idle", "60fps animations"]);
279 Ok(CallToolResult::success(vec![Content::text(builder.build())]))
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286 #[tokio::test]
287 async fn test_generate_testing() {
288 let tool = TestingIntegrationTool::new();
289 let result = tool.generate_testing_integration(TestingIntegrationParams { test_type: None, include_examples: Some(true) }).await.unwrap();
290 assert!(result.is_error.is_none() || result.is_error == Some(false));
291 }
292
293 #[tokio::test]
294 async fn test_generate_ui_automation_tests_success() {
295 let tool = TestingIntegrationTool::new();
296 let params = UITestParams {
297 test_type: Some("button_click".to_string()),
298 include_page_objects: Some(true),
299 };
300 let result = tool.generate_ui_automation_tests(params).await.unwrap();
301 assert!(result.is_error.is_none() || result.is_error == Some(false));
302 }
303
304 #[tokio::test]
305 async fn test_generate_mocks_and_builders_success() {
306 let tool = TestingIntegrationTool::new();
307 let params = MocksAndBuildersParams {
308 entity_name: "Order".to_string(),
309 include_fluent_builder: Some(true),
310 };
311 let result = tool.generate_mocks_and_builders(params).await.unwrap();
312 assert!(result.is_error.is_none() || result.is_error == Some(false));
313 }
314
315 #[tokio::test]
316 async fn test_generate_mocks_and_builders_empty_name() {
317 let tool = TestingIntegrationTool::new();
318 let params = MocksAndBuildersParams {
319 entity_name: "".to_string(),
320 include_fluent_builder: None,
321 };
322 let result = tool.generate_mocks_and_builders(params).await;
323 assert!(result.is_err());
324 }
325
326 #[tokio::test]
327 async fn test_generate_performance_tests_success() {
328 let tool = TestingIntegrationTool::new();
329 let params = PerformanceTestParams {
330 test_area: Some("rendering".to_string()),
331 include_profiling: Some(true),
332 };
333 let result = tool.generate_performance_tests(params).await.unwrap();
334 assert!(result.is_error.is_none() || result.is_error == Some(false));
335 }
336}