complete milestone 1

This commit is contained in:
2026-01-16 22:17:12 +01:00
parent 8de36aa975
commit baa77d4c95
8 changed files with 524 additions and 78 deletions

View File

@@ -17,25 +17,56 @@ bool HierarchyReader::Reload() {
return false;
}
// Retry a few times with delay to handle file write race conditions
for (int attempt = 0; attempt < 5; ++attempt) {
// Retry with increasing delays to handle file write race conditions
const int maxAttempts = 10;
const int baseDelayMs = 30;
for (int attempt = 0; attempt < maxAttempts; ++attempt) {
if (attempt > 0) {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
// Exponential backoff: 30, 60, 120, ... ms
int delayMs = baseDelayMs * (1 << std::min(attempt - 1, 4));
std::this_thread::sleep_for(std::chrono::milliseconds(delayMs));
}
std::ifstream file(m_path);
if (!file.is_open()) {
continue; // File might not exist yet, retry
// Read entire file into string first (to avoid partial reads)
std::string content;
{
std::ifstream file(m_path, std::ios::binary);
if (!file.is_open()) {
continue; // File might not exist yet, retry
}
// Get file size
file.seekg(0, std::ios::end);
auto size = file.tellg();
if (size <= 0) {
continue; // Empty file, retry
}
file.seekg(0, std::ios::beg);
// Read content
content.resize(static_cast<size_t>(size));
file.read(content.data(), size);
if (!file.good()) {
continue; // Read error, retry
}
}
// Check for valid JSON ending (should end with "}")
size_t lastNonSpace = content.find_last_not_of(" \t\n\r");
if (lastNonSpace == std::string::npos || content[lastNonSpace] != '}') {
continue; // File is incomplete, retry
}
try {
m_json = nlohmann::json::parse(file);
m_json = nlohmann::json::parse(content);
m_loaded = true;
return true;
} catch (const nlohmann::json::parse_error& e) {
// File might be partially written, retry
if (attempt == 4) {
std::cerr << "Failed to parse hierarchy JSON after 5 attempts: " << e.what() << std::endl;
if (attempt == maxAttempts - 1) {
std::cerr << "Failed to parse hierarchy JSON after " << maxAttempts << " attempts: " << e.what() << std::endl;
}
}
}

View File

@@ -6,6 +6,7 @@
#include <filesystem>
#include <thread>
#include <chrono>
#include <optional>
using namespace mosis::test;
@@ -104,26 +105,88 @@ bool ClickByClassIndex(TestContext& ctx, const std::string& className, int index
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) {
// Click back button (app-bar-nav) up to 5 times to ensure we're at home
// 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();
// 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);
}
// 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;
}
std::this_thread::sleep_for(std::chrono::milliseconds(400));
// 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));
}
// Extra wait for animations to fully complete and hierarchy to update
std::this_thread::sleep_for(std::chrono::milliseconds(800));
// 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

View File

@@ -6,8 +6,35 @@
namespace mosis::test {
// Callback for EnumWindows to find window by partial title match
struct FindWindowData {
std::string searchTitle;
HWND foundHwnd = nullptr;
};
static BOOL CALLBACK EnumWindowsCallback(HWND hwnd, LPARAM lParam) {
auto* data = reinterpret_cast<FindWindowData*>(lParam);
char title[256] = {0};
GetWindowTextA(hwnd, title, sizeof(title));
// Check if the window title contains our search string
if (std::string(title).find(data->searchTitle) != std::string::npos) {
data->foundHwnd = hwnd;
return FALSE; // Stop enumeration
}
return TRUE; // Continue enumeration
}
bool WindowController::FindWindow(const std::string& title) {
m_hwnd = ::FindWindowA(nullptr, title.c_str());
// Use EnumWindows to find window by partial title match
// This allows finding "Mosis Designer - home.rml" when searching for "Mosis Designer"
FindWindowData data;
data.searchTitle = title;
EnumWindows(EnumWindowsCallback, reinterpret_cast<LPARAM>(&data));
m_hwnd = data.foundHwnd;
if (!m_hwnd) {
return false;
}
@@ -80,24 +107,33 @@ LPARAM WindowController::PhoneToClientLParam(int phoneX, int phoneY) {
bool WindowController::SendMouseDown(int phoneX, int phoneY) {
if (!m_hwnd) return false;
// Convert to screen coordinates for SendInput
// Convert phone coordinates to client coordinates
int clientX = static_cast<int>(phoneX * m_info.scaleX);
int clientY = static_cast<int>(phoneY * m_info.scaleY);
// Get DPI info for debugging
UINT dpi = GetDpiForWindow(m_hwnd);
// Calculate screen coordinates from client position
// On DPI-aware systems, Windows APIs return consistent coordinate spaces
int screenX = m_info.clientX + clientX;
int screenY = m_info.clientY + clientY;
// Use SendInput for proper GLFW compatibility
// First move the cursor to the position
SetCursorPos(screenX, screenY);
// Ensure window is foreground before clicking
SetForegroundWindow(m_hwnd);
Sleep(10); // Small delay
// Use SendInput for GLFW compatibility
SetCursorPos(screenX, screenY);
Sleep(10); // Small delay for cursor move
// Send mouse down via SendInput
INPUT input = {};
input.type = INPUT_MOUSE;
input.mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
SendInput(1, &input, sizeof(INPUT));
std::cout << "MouseDown at phone(" << phoneX << "," << phoneY << ") -> screen("
<< screenX << "," << screenY << ")" << std::endl;
<< screenX << "," << screenY << ") dpi=" << dpi << std::endl;
return true;
}
@@ -105,12 +141,15 @@ bool WindowController::SendMouseDown(int phoneX, int phoneY) {
bool WindowController::SendMouseUp(int phoneX, int phoneY) {
if (!m_hwnd) return false;
// Send mouse up via SendInput
Sleep(10); // Small delay before release
INPUT input = {};
input.type = INPUT_MOUSE;
input.mi.dwFlags = MOUSEEVENTF_LEFTUP;
SendInput(1, &input, sizeof(INPUT));
Sleep(10); // Small delay after release
std::cout << "MouseUp at phone(" << phoneX << "," << phoneY << ")" << std::endl;
return true;