// Mosis Designer Test - Automated UI Testing // Sends input events directly to the designer window using element coordinates from hierarchy dump #include "test_runner.h" #include #include #include #include #include using namespace mosis::test; // Helper: Scale coordinates from hierarchy (logical) to window (physical) space void ScaleToPhysical(TestContext& ctx, int& x, int& y) { // The hierarchy reports coordinates in RmlUi's logical space // which may be DPI-scaled. We need to convert to physical window coordinates. int hierarchyWidth = ctx.hierarchy.GetWidth(); int hierarchyHeight = ctx.hierarchy.GetHeight(); int windowWidth = ctx.window.GetInfo().clientWidth; int windowHeight = ctx.window.GetInfo().clientHeight; if (hierarchyWidth > 0 && hierarchyHeight > 0) { x = static_cast(x * static_cast(windowWidth) / hierarchyWidth); y = static_cast(y * static_cast(windowHeight) / hierarchyHeight); } } // Helper: Click on an element found by ID in the hierarchy bool ClickById(TestContext& ctx, const std::string& id) { ctx.hierarchy.Reload(); // Debug: Print hierarchy info std::cout << " Hierarchy: " << ctx.hierarchy.GetWidth() << "x" << ctx.hierarchy.GetHeight() << ", screen: " << ctx.hierarchy.GetScreenName() << std::endl; auto element = ctx.hierarchy.FindById(id); if (!element) { std::cerr << " Element not found: #" << id << std::endl; // List available IDs for debugging auto allElements = ctx.hierarchy.GetAllElements(); std::cerr << " Available IDs: "; for (const auto& e : allElements) { if (!e.id.empty()) { std::cerr << "#" << e.id << " "; } } std::cerr << std::endl; return false; } if (!element->visible) { std::cerr << " Element not visible: #" << id << std::endl; return false; } int x = element->bounds.centerX(); int y = element->bounds.centerY(); int logicalX = x, logicalY = y; ScaleToPhysical(ctx, x, y); std::cout << " Clicking #" << id << " at logical(" << logicalX << "," << logicalY << ") -> physical(" << x << "," << y << ")" << std::endl; ctx.window.SendClick(x, y); return true; } // Helper: Click on an element found by class name (first visible match) bool ClickByClass(TestContext& ctx, const std::string& className) { ctx.hierarchy.Reload(); auto elements = ctx.hierarchy.FindByClass(className); for (const auto& element : elements) { if (element.visible && element.bounds.width > 0 && element.bounds.height > 0) { int x = element.bounds.centerX(); int y = element.bounds.centerY(); ScaleToPhysical(ctx, x, y); std::cout << " Clicking ." << className << " at physical(" << x << "," << y << ")" << std::endl; ctx.window.SendClick(x, y); return true; } } std::cerr << " No visible element found with class: " << className << std::endl; return false; } // Helper: Click on an element found by class name at a specific index bool ClickByClassIndex(TestContext& ctx, const std::string& className, int index) { ctx.hierarchy.Reload(); auto elements = ctx.hierarchy.FindByClass(className); int visibleIndex = 0; for (const auto& element : elements) { if (element.visible && element.bounds.width > 0 && element.bounds.height > 0) { if (visibleIndex == index) { int x = element.bounds.centerX(); int y = element.bounds.centerY(); ScaleToPhysical(ctx, x, y); std::cout << " Clicking ." << className << "[" << index << "] at physical(" << x << "," << y << ")" << std::endl; ctx.window.SendClick(x, y); return true; } visibleIndex++; } } std::cerr << " Element ." << className << "[" << index << "] not found (only " << visibleIndex << " visible)" << std::endl; return false; } // Helper: Wait for hierarchy to update to expected screen bool WaitForScreen(TestContext& ctx, const std::string& expectedScreen, int timeoutMs = 3000) { auto startTime = std::chrono::steady_clock::now(); while (true) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); ctx.hierarchy.Reload(); std::string currentScreen = ctx.hierarchy.GetScreenName(); if (currentScreen.find(expectedScreen) != std::string::npos) { return true; } auto elapsed = std::chrono::steady_clock::now() - startTime; if (std::chrono::duration_cast(elapsed).count() >= timeoutMs) { return false; } } } // Helper: Find a back button in the hierarchy std::optional FindBackButton(TestContext& ctx) { // Try to find back button by class (app-bar-nav is used in most screens) auto elements = ctx.hierarchy.FindByClass("app-bar-nav"); for (const auto& elem : elements) { if (elem.visible && elem.bounds.width > 0 && elem.bounds.height > 0) { return elem; } } // Try browser-specific back button elements = ctx.hierarchy.FindByClass("browser-nav-btn"); for (const auto& elem : elements) { if (elem.visible && elem.bounds.width > 0 && elem.bounds.height > 0) { return elem; } } return std::nullopt; } // Helper: Go back to home screen by clicking back button multiple times void GoHome(TestContext& ctx) { // Wait longer for hierarchy file to be updated from any previous navigation // The designer writes hierarchy every frame, but there can be a race condition std::this_thread::sleep_for(std::chrono::milliseconds(1500)); for (int i = 0; i < 5; ++i) { // Wait then reload to get fresh hierarchy data std::this_thread::sleep_for(std::chrono::milliseconds(300)); ctx.hierarchy.Reload(); // Check hierarchy to see if dock-phone exists (indicating home screen) auto dockPhone = ctx.hierarchy.FindById("dock-phone"); if (dockPhone && dockPhone->visible) { std::cout << " GoHome: At home screen (dock-phone found)" << std::endl; break; } // Find back button from hierarchy auto backBtn = FindBackButton(ctx); int x, y; if (backBtn) { x = backBtn->bounds.centerX(); y = backBtn->bounds.centerY(); std::cout << " GoHome: Found back button at (" << x << "," << y << ")" << std::endl; } else { // Fallback to default position if no back button found x = 48; y = 36; std::cout << " GoHome: No back button found, using default position" << std::endl; } ScaleToPhysical(ctx, x, y); std::cout << " GoHome: Clicking back at (" << x << "," << y << ")" << std::endl; ctx.window.SendClick(x, y); // Wait for navigation animation to complete std::this_thread::sleep_for(std::chrono::milliseconds(1000)); } // Final verification - wait and reload hierarchy std::this_thread::sleep_for(std::chrono::milliseconds(500)); ctx.hierarchy.Reload(); std::cout << " GoHome: Final screen = " << ctx.hierarchy.GetScreenName() << std::endl; } // Test: Navigate to dialer by clicking Phone dock icon bool TestNavigateToDialer(TestContext& ctx) { // Ensure we start from home screen GoHome(ctx); ctx.log.Clear(); // Click on Phone dock icon (dock-phone id) if (!ClickById(ctx, "dock-phone")) { return false; } // Wait for navigation std::this_thread::sleep_for(std::chrono::milliseconds(1000)); // Reload log and check for navigation ctx.log.Reload(); return ctx.log.Contains("Loaded screen: apps/dialer/dialer.rml") || ctx.log.Contains("apps/dialer"); } // Test: Navigate to messages bool TestNavigateToMessages(TestContext& ctx) { // Ensure we start from home screen GoHome(ctx); ctx.log.Clear(); // Click on Messages dock icon if (!ClickById(ctx, "dock-messages")) { return false; } std::this_thread::sleep_for(std::chrono::milliseconds(1000)); ctx.log.Reload(); return ctx.log.Contains("Loaded screen: apps/messages/messages.rml"); } // Test: Navigate to contacts bool TestNavigateToContacts(TestContext& ctx) { // Ensure we start from home screen GoHome(ctx); ctx.log.Clear(); // Click on Contacts dock icon if (!ClickById(ctx, "dock-contacts")) { return false; } std::this_thread::sleep_for(std::chrono::milliseconds(1000)); ctx.log.Reload(); return ctx.log.Contains("Loaded screen: apps/contacts/contacts.rml"); } // Test: Navigate to browser bool TestNavigateToBrowser(TestContext& ctx) { // Ensure we start from home screen GoHome(ctx); ctx.log.Clear(); // Click on Browser dock icon if (!ClickById(ctx, "dock-browser")) { return false; } std::this_thread::sleep_for(std::chrono::milliseconds(1000)); ctx.log.Reload(); return ctx.log.Contains("Loaded screen: apps/browser/browser.rml"); } // Test: Navigate to settings (in app grid, not dock) bool TestNavigateToSettings(TestContext& ctx) { // Ensure we start from home screen GoHome(ctx); ctx.log.Clear(); // Settings is in the app grid - find by ID if (!ClickById(ctx, "app-settings")) { return false; } std::this_thread::sleep_for(std::chrono::milliseconds(1000)); ctx.log.Reload(); return ctx.log.Contains("Loaded screen: apps/settings/settings.rml"); } // Test: Multiple navigation sequence from home bool TestNavigationSequence(TestContext& ctx) { // Ensure we start from home screen GoHome(ctx); ctx.log.Clear(); // Go to dialer from home if (!ClickById(ctx, "dock-phone")) { std::cerr << " Failed to click dock-phone" << std::endl; return false; } std::this_thread::sleep_for(std::chrono::milliseconds(1000)); // Go back to home GoHome(ctx); // Go to messages from home if (!ClickById(ctx, "dock-messages")) { std::cerr << " Failed to click dock-messages" << std::endl; return false; } std::this_thread::sleep_for(std::chrono::milliseconds(1000)); // Go back to home GoHome(ctx); // Go to contacts from home if (!ClickById(ctx, "dock-contacts")) { std::cerr << " Failed to click dock-contacts" << std::endl; return false; } std::this_thread::sleep_for(std::chrono::milliseconds(1000)); ctx.log.Reload(); // Verify all navigations happened bool hasDialer = ctx.log.Contains("apps/dialer"); bool hasMessages = ctx.log.Contains("apps/messages"); bool hasContacts = ctx.log.Contains("apps/contacts"); std::cout << " Dialer: " << (hasDialer ? "yes" : "no") << std::endl; std::cout << " Messages: " << (hasMessages ? "yes" : "no") << std::endl; std::cout << " Contacts: " << (hasContacts ? "yes" : "no") << std::endl; return hasDialer && hasMessages && hasContacts; } int main(int argc, char* argv[]) { std::cout << "Mosis Designer Test v0.2.0" << std::endl; std::cout << "==========================" << std::endl; std::cout << "Using UI hierarchy for element coordinates" << std::endl; // Determine paths // exe is at: designer-test/build/Debug/designer-test.exe // project root is: MosisService/ std::filesystem::path exePath = std::filesystem::absolute(argv[0]).parent_path(); std::filesystem::path projectRoot = exePath.parent_path().parent_path().parent_path(); // Up 3 levels std::filesystem::path designerPath = projectRoot / "designer" / "build" / "Debug" / "mosis-designer.exe"; std::filesystem::path documentPath = projectRoot / "src" / "main" / "assets" / "apps" / "home" / "home.rml"; std::filesystem::path logPath = exePath / "designer_test.log"; std::filesystem::path hierarchyPath = exePath / "ui_hierarchy.json"; std::filesystem::path resultsPath = exePath / "test_results.json"; // Allow command-line overrides for (int i = 1; i < argc; ++i) { std::string arg = argv[i]; if (arg == "--designer" && i + 1 < argc) { designerPath = argv[++i]; } else if (arg == "--document" && i + 1 < argc) { documentPath = argv[++i]; } else if (arg == "--log" && i + 1 < argc) { logPath = argv[++i]; } else if (arg == "--hierarchy" && i + 1 < argc) { hierarchyPath = argv[++i]; } else if (arg == "--results" && i + 1 < argc) { resultsPath = argv[++i]; } else if (arg == "--help") { std::cout << "Usage: designer-test [options]" << std::endl; std::cout << "Options:" << std::endl; std::cout << " --designer PATH Path to mosis-designer.exe" << std::endl; std::cout << " --document PATH Path to initial RML document" << std::endl; std::cout << " --log PATH Path for log file" << std::endl; std::cout << " --hierarchy PATH Path for UI hierarchy dump file" << std::endl; std::cout << " --results PATH Path for JSON test results" << std::endl; std::cout << " --help Show this help" << std::endl; return 0; } } std::cout << "Designer: " << designerPath << std::endl; std::cout << "Document: " << documentPath << std::endl; std::cout << "Log file: " << logPath << std::endl; std::cout << "Hierarchy: " << hierarchyPath << std::endl; // Check if designer exists if (!std::filesystem::exists(designerPath)) { std::cerr << "ERROR: Designer not found at: " << designerPath << std::endl; std::cerr << "Build the designer first: cd designer && cmake --build build --config Debug" << std::endl; return 1; } if (!std::filesystem::exists(documentPath)) { std::cerr << "ERROR: Document not found at: " << documentPath << std::endl; return 1; } // Create test runner TestRunner runner; runner.SetDesignerPath(designerPath); runner.SetDocumentPath(documentPath); runner.SetLogPath(logPath); runner.SetHierarchyPath(hierarchyPath); runner.SetStartupTimeoutMs(15000); runner.SetActionDelayMs(500); // Register tests runner.AddTest("Navigate to Dialer", TestNavigateToDialer); runner.AddTest("Navigate to Messages", TestNavigateToMessages); runner.AddTest("Navigate to Contacts", TestNavigateToContacts); runner.AddTest("Navigate to Browser", TestNavigateToBrowser); runner.AddTest("Navigation Sequence", TestNavigationSequence); // Start designer std::cout << "\nStarting designer..." << std::endl; if (!runner.StartDesigner()) { std::cerr << "Failed to start designer" << std::endl; return 1; } std::cout << "Designer started successfully" << std::endl; std::cout << "Window info:" << std::endl; const auto& info = runner.GetWindow().GetInfo(); std::cout << " Scale: " << info.scaleX << " x " << info.scaleY << std::endl; // Run tests auto results = runner.RunAll(); // Save results results.SaveToFile(resultsPath); // Stop designer std::cout << "Stopping designer..." << std::endl; runner.StopDesigner(); // Return exit code based on results return (results.failed + results.errors) > 0 ? 1 : 0; }