work in progress
This commit is contained in:
364
designer-test/src/main.cpp
Normal file
364
designer-test/src/main.cpp
Normal file
@@ -0,0 +1,364 @@
|
||||
// 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>
|
||||
|
||||
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: Go back to home screen by clicking back button multiple times
|
||||
void GoHome(TestContext& ctx) {
|
||||
// Click back button (app-bar-nav) up to 5 times to ensure we're at home
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
ctx.hierarchy.Reload();
|
||||
// Look for the back button by class
|
||||
auto elements = ctx.hierarchy.FindByClass("app-bar-nav");
|
||||
if (!elements.empty()) {
|
||||
auto& btn = elements[0];
|
||||
if (btn.visible && btn.bounds.width > 0) {
|
||||
int x = btn.bounds.centerX();
|
||||
int y = btn.bounds.centerY();
|
||||
ScaleToPhysical(ctx, x, y);
|
||||
ctx.window.SendClick(x, y);
|
||||
}
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(400));
|
||||
}
|
||||
// Extra wait for animations to fully complete and hierarchy to update
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(800));
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user