428 lines
15 KiB
C++
428 lines
15 KiB
C++
// 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 <iostream>
|
|
#include <filesystem>
|
|
#include <thread>
|
|
#include <chrono>
|
|
#include <optional>
|
|
|
|
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<int>(x * static_cast<float>(windowWidth) / hierarchyWidth);
|
|
y = static_cast<int>(y * static_cast<float>(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<std::chrono::milliseconds>(elapsed).count() >= timeoutMs) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper: Find a back button in the hierarchy
|
|
std::optional<ElementInfo> 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;
|
|
}
|