Files
MosisService/TESTING.md
2026-01-16 12:43:06 +01:00

398 lines
9.7 KiB
Markdown

# Mosis UI Testing Framework
This document describes the automated UI testing framework for Mosis virtual smartphone.
## Overview
The testing framework enables automated validation of UI behavior through:
1. **UI Hierarchy Inspection**: JSON dump of all UI elements with bounds
2. **Input Simulation**: Mouse clicks via Windows SendInput API
3. **Log Verification**: Check navigation events via log file parsing
4. **Test Results**: JSON output compatible with CI/CD pipelines
## Components
### Designer (mosis-designer.exe)
The desktop designer serves as the test target. When launched with testing options:
```bash
mosis-designer.exe home.rml --log test.log --hierarchy hierarchy.json
```
- `--log`: Writes all RmlUi INFO logs to file (navigation events, errors)
- `--hierarchy`: Dumps UI element tree to JSON each frame
### Test Runner (designer-test.exe)
Automated test executor that:
1. Launches designer with testing options
2. Waits for window to appear
3. Reads UI hierarchy to find elements
4. Sends input events to simulate user interaction
5. Verifies results via log file
6. Outputs JSON test results
## UI Hierarchy Format
The hierarchy JSON contains all visible UI elements:
```json
{
"timestamp": 1705312200,
"screen": "apps/home/home.rml",
"resolution": {"width": 677, "height": 1202},
"elements": {
"tag": "body",
"id": "",
"classes": ["home-screen"],
"bounds": {"x": 0, "y": 0, "width": 677, "height": 1202},
"visible": true,
"text": null,
"children": [
{
"tag": "div",
"id": "dock-phone",
"classes": ["dock-item"],
"bounds": {"x": 85, "y": 1138, "width": 56, "height": 56},
"visible": true,
"text": null,
"children": [...]
}
]
}
}
```
### Key Fields
| Field | Description |
|-------|-------------|
| `tag` | HTML element tag (div, span, img, input) |
| `id` | Element ID attribute (empty if none) |
| `classes` | Array of CSS class names |
| `bounds` | Position and size in logical pixels |
| `visible` | Whether element is displayed |
| `text` | Text content (for leaf text nodes) |
| `children` | Nested child elements |
### Resolution Note
The `resolution` field contains RmlUi's logical dimensions, which may differ from physical window size due to DPI scaling. Tests must scale coordinates:
```cpp
// Scale from hierarchy (logical) to window (physical)
int physicalX = logicalX * windowWidth / hierarchyWidth;
int physicalY = logicalY * windowHeight / hierarchyHeight;
```
## Writing Tests
### Test Function Signature
```cpp
bool MyTest(TestContext& ctx);
```
TestContext provides:
- `ctx.window` - WindowController for input
- `ctx.log` - LogParser for log verification
- `ctx.hierarchy` - HierarchyReader for element lookup
### Finding Elements
```cpp
// By ID
auto element = ctx.hierarchy.FindById("dock-phone");
// By class (returns all matches)
auto buttons = ctx.hierarchy.FindByClass("btn-icon");
// By tag
auto divs = ctx.hierarchy.FindByTag("div");
```
### Clicking Elements
```cpp
bool ClickById(TestContext& ctx, const std::string& id) {
ctx.hierarchy.Reload(); // Get fresh hierarchy
auto element = ctx.hierarchy.FindById(id);
if (!element || !element->visible) {
return false;
}
// Get center point
int x = element->bounds.centerX();
int y = element->bounds.centerY();
// Scale to physical coordinates
ScaleToPhysical(ctx, x, y);
// Send click
ctx.window.SendClick(x, y);
return true;
}
```
### Verifying Navigation
```cpp
bool TestNavigateToDialer(TestContext& ctx) {
GoHome(ctx); // Return to home screen
ctx.log.Clear(); // Clear previous logs
if (!ClickById(ctx, "dock-phone")) {
return false;
}
// Wait for navigation animation
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
// Check log for navigation event
ctx.log.Reload();
return ctx.log.Contains("Loaded screen: apps/dialer/dialer.rml");
}
```
### Navigation Helpers
```cpp
// Go back to home screen by clicking back buttons
void GoHome(TestContext& ctx) {
for (int i = 0; i < 5; ++i) {
ctx.hierarchy.Reload();
auto elements = ctx.hierarchy.FindByClass("app-bar-nav");
if (!elements.empty() && elements[0].visible) {
int x = elements[0].bounds.centerX();
int y = elements[0].bounds.centerY();
ScaleToPhysical(ctx, x, y);
ctx.window.SendClick(x, y);
}
std::this_thread::sleep_for(std::chrono::milliseconds(400));
}
std::this_thread::sleep_for(std::chrono::milliseconds(800));
}
```
## Registering Tests
In `main.cpp`:
```cpp
TestRunner runner;
runner.SetDesignerPath(designerPath);
runner.SetDocumentPath(documentPath);
runner.SetLogPath(logPath);
runner.SetHierarchyPath(hierarchyPath);
// 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);
// Run all tests
auto results = runner.RunAll();
results.SaveToFile(resultsPath);
```
## Test Results Format
```json
{
"name": "Mosis Designer UI Tests",
"summary": {
"passed": 5,
"failed": 0,
"skipped": 0,
"errors": 0,
"total": 5,
"duration_ms": 31162.3
},
"tests": [
{
"name": "Navigate to Dialer",
"status": "passed",
"message": "Test passed",
"duration_ms": 4216.88
},
{
"name": "Navigate to Browser",
"status": "passed",
"message": "Test passed",
"duration_ms": 4067.34
}
]
}
```
## Running Tests
### Command Line
```bash
# Run with defaults (auto-detects paths)
designer-test.exe
# Override paths
designer-test.exe --designer /path/to/mosis-designer.exe \
--document /path/to/home.rml \
--log /path/to/output.log \
--hierarchy /path/to/hierarchy.json \
--results /path/to/results.json
```
### Exit Codes
- `0`: All tests passed
- `1`: One or more tests failed
## Best Practices
### Element Identification
1. **Use IDs** for clickable elements that tests need to find
2. **Use semantic classes** like `app-bar-nav` for navigation buttons
3. **Avoid relying on position** - use hierarchy lookup instead
### Timing
1. **Wait after navigation** (1000ms) for animations to complete
2. **Reload hierarchy** before each element lookup
3. **Use retry logic** for file reads (hierarchy may be mid-write)
### Test Independence
1. **Start from known state** - call GoHome() at start of each test
2. **Clear logs** before verification
3. **Don't depend on previous test state**
## Adding Test IDs to RML
When creating new screens, add IDs to interactive elements:
```html
<!-- Good: Has ID for testing -->
<div id="dock-phone" class="dock-item" onclick="navigateTo('dialer')">
<img src="../../icons/phone.tga"/>
</div>
<!-- Good: Has class for back button detection -->
<div class="app-bar-nav btn-icon" onclick="goBack()">
<img src="../../icons/back.tga"/>
</div>
```
## Troubleshooting
### Element Not Found
- Verify the screen has loaded (check hierarchy `screen` field)
- Check if element has the expected ID/class
- Ensure element is visible (`visible: true`)
### Click Not Registering
- Check coordinate scaling (hierarchy vs window size)
- Increase wait time for animations
- Verify window is in foreground
### Timing Issues
- Increase delays after GoHome() and navigation
- Add retries for hierarchy reload
- Check for race conditions in file I/O
## Android Testing
The Android app supports event injection for automated testing.
### Event Injection Methods
The `MainActivity` provides these methods for programmatic testing:
```kotlin
// Inject touch events with normalized coordinates (0.0-1.0)
MainActivity.instance?.injectTouchDown(x, y)
MainActivity.instance?.injectTouchMove(x, y)
MainActivity.instance?.injectTouchUp(x, y)
MainActivity.instance?.injectClick(x, y) // Down + delay + Up
```
### Broadcast-Based Injection
For external test frameworks or ADB, send broadcasts:
```bash
# Inject a click at center (0.5, 0.5)
adb shell am broadcast -a com.omixlab.mosis.INJECT_TOUCH \
--es touch_type "click" \
--ef x 0.5 \
--ef y 0.5
# Inject touch down
adb shell am broadcast -a com.omixlab.mosis.INJECT_TOUCH \
--es touch_type "down" \
--ef x 0.2 \
--ef y 0.9
# Inject touch up
adb shell am broadcast -a com.omixlab.mosis.INJECT_TOUCH \
--es touch_type "up" \
--ef x 0.2 \
--ef y 0.9
```
### UI Element Coordinates
Dock items are positioned at the bottom of the screen. Approximate normalized positions:
| Element | X | Y |
|---------|---|---|
| dock-phone | 0.16 | 0.97 |
| dock-messages | 0.39 | 0.97 |
| dock-contacts | 0.61 | 0.97 |
| dock-browser | 0.84 | 0.97 |
### Instrumentation Tests
For full integration tests, use Android's instrumentation framework:
```kotlin
@RunWith(AndroidJUnit4::class)
class NavigationTest {
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun testNavigateToDialer() {
// Wait for service to connect
Thread.sleep(2000)
// Click on phone dock icon (normalized coords)
activityRule.scenario.onActivity { activity ->
activity.injectClick(0.16f, 0.97f)
}
// Wait for navigation
Thread.sleep(1000)
// Verify navigation occurred (check logs or UI state)
}
}
```
## Future Improvements
- [ ] Action recording (capture user interactions)
- [ ] Screenshot comparison (visual regression testing)
- [ ] Android instrumentation test suite
- [ ] Parallel test execution
- [ ] Test coverage reporting
- [ ] Cross-platform test runner (desktop + Android)