# Remote Protocol Unit Test Framework
The remote protocol unit test framework allows testing GacUI applications without real OS windows or rendering. It reuses the same remote protocol architecture as production remote deployment, replacing the renderer side with a mock implementation (`UnitTestRemoteProtocol`) that captures rendering results as snapshots and feeds simulated user input through the protocol pipeline. The GacUI core side runs identically to production.
Implementation lives in `Source/UnitTestUtilities/`.
## Comprehensive Example
Below is a complete example demonstrating the key concepts of the framework: test executable setup, GacUI XML resource definition, frame-based interaction with named snapshots, control lookup, and mouse input simulation.
### Test Executable Entry Point
The test executable's `main` function initializes the framework once, runs all test cases, and finalizes:
```cpp
int UnitTestMain(int argc, T* argv[])
{
UnitTestFrameworkConfig config;
config.snapshotFolder = GetTestSnapshotPath();
config.resourceFolder = GetTestDataPath();
GacUIUnitTest_Initialize(&config);
int result = UnitTest::RunAndDisposeTests(argc, argv);
GacUIUnitTest_Finalize();
return result;
}
```
### A Test Case: Clicking a Button
This example defines a GacUI XML resource with a window containing an OK button, then tests hovering, pressing, and releasing the button in separate frames. Each frame callback describes the action it performs; the frame name is assigned to the **rendering snapshot that results from the previous frame's action**.
```cpp
const auto resource = LR"GacUISrc(
)GacUISrc";
TEST_CASE(L"Click Button")
{
GacUIUnitTest_SetGuiMainProxy([](UnitTestRemoteProtocol* protocol, IUnitTestContext*)
{
// Frame 0: snapshot named "Ready" — shows the initial rendering after the window opens.
// Callback action: move the mouse on the OK button.
protocol->OnNextIdleFrame(L"Ready", [=]()
{
auto window = GetApplication()->GetMainWindow();
auto buttonOK = TryFindObjectByName(window, L"buttonOK");
// MouseMove(location) + LClick() could be merged into one operation
// LClick(location) will perform both
// but when the side effect of MouseMove is important
// you are given choices
protocol->MouseMove(protocol->LocationOf(buttonOK));
});
// Frame 1: snapshot named "Hover" — shows the button in hover state (result of MouseMove above).
// Callback action: click the OK button, triggering Clicked → window closes.
protocol->OnNextIdleFrame(L"Hover", [=]()
{
protocol->LClick();
});
// If the user interaction above does not cause the application to exit,
// an additional frame is needed to close the window explicitly:
//
// protocol->OnNextIdleFrame(L"Frame name describing what the previous frame does", [=]()
// {
// auto window = GetApplication()->GetMainWindow();
// window->Hide();
// });
});
GacUIUnitTest_StartFast_WithResourceAsText(
WString::Unmanaged(L"UnitTestFramework/WindowWithOKButton"),
WString::Unmanaged(L"gacuisrc_unittest::MainWindow"),
resource
);
});
```
### What This Example Demonstrates
- **`GacUIUnitTest_SetGuiMainProxy`**: Registers the test's frame callback function. It receives `UnitTestRemoteProtocol*` (for input simulation and snapshot access) and `IUnitTestContext*`.
- **`OnNextIdleFrame(name, callback)`**: Registers a callback to run after the next rendering frame settles. The `name` is attached to the snapshot that was just captured (the rendering result), not to what the callback will do.
- **`TryFindObjectByName(window, name)`**: Looks up a named control from the GacUI XML resource by its `ref.Name`.
- **`protocol->LocationOf(control)`**: Computes the absolute screen coordinate of the center of a control, for use with mouse input methods.
- **`LClick(location)`**: Simulates a full left mouse click (mouse move + button down + button up) at the given location. Use this for simple clicks instead of separate `_LDown`/`_LUp` calls.
- **`GacUIUnitTest_StartFast_WithResourceAsText`**: Compiles the XML resource, registers the theme, creates the window, runs the application, and captures snapshots — all in one call.
- **Frame name semantics**: `"Ready"` names the initial rendering. Each snapshot file (`frame_0.json`) records the full rendering DOM at that point.
- **Closing the window**: The button's click handler uses `InvokeInMainThread` to defer `self.Close()`. This is necessary because `Close()` would otherwise be called synchronously inside the IO event dispatch, which could block the frame callback (see the blocking function caveat below). If user interaction does not cause the application to exit, an extra frame must be added at the end to call `window->Hide()` explicitly.
## Test Executable Initialization and Finalization
### Global Setup (GacUIUnitTest_Initialize)
Each test executable's `main` calls `GacUIUnitTest_Initialize(&config)` with a `UnitTestFrameworkConfig` containing `snapshotFolder` and `resourceFolder`. This function:
- Sets `GACUI_UNITTEST_ONLY_SKIP_THREAD_LOCAL_STORAGE_DISPOSE_STORAGES` and `GACUI_UNITTEST_ONLY_SKIP_TYPE_AND_PLUGIN_LOAD_UNLOAD` to `true`, allowing the type manager and plugin system to be loaded once and reused across all test cases.
- Calls `GetGlobalTypeManager()->Load()` and `GetPluginManager()->Load(true, false)` once.
### Global Teardown (GacUIUnitTest_Finalize)
Called after all tests complete. Tears down the type manager and plugin manager.
## Per-Test-Case Setup and the Proxy Chaining Pattern
### Registering Frame Callbacks
Each test case follows a standard pattern:
1. Call `GacUIUnitTest_SetGuiMainProxy(callback)` to register a `UnitTestMainFunc` (signature: `void(UnitTestRemoteProtocol*, IUnitTestContext*)`). This is the test's frame-based action callback that registers idle frame handlers via `OnNextIdleFrame`.
2. Optionally call `GacUIUnitTest_LinkGuiMainProxy(linkCallback)` to wrap additional setup around the test proxy.
3. Call one of the start functions to launch the test.
### Proxy Chaining (GacUIUnitTest_LinkGuiMainProxy)
`GacUIUnitTest_LinkGuiMainProxy` captures the current proxy as `previousMainProxy` and installs a new proxy that calls the link function with the previous proxy as a parameter. This enables decorator-style composition — each link layer can perform setup before delegating to the inner proxy and cleanup after.
### GacUIUnitTest_StartFast_WithResourceAsText
This template function is the most commonly used entry point. It internally calls `GacUIUnitTest_LinkGuiMainProxy` to inject:
1. Sending `OnControllerConnect` with `ControllerGlobalConfig` to the protocol.
2. Registering a theme (e.g., `DarkSkin`).
3. Compiling the GacUI XML resource via `GacUIUnitTest_CompileAndLoad`.
4. Creating the main window from the resource via `Value::Create(windowTypeFullName)`.
5. Calling `previousMainProxy(protocol, context)` to let the test register its frame callbacks.
6. Running the application via `GetApplication()->Run(window)`.
7. Unregistering the theme on cleanup.
It also saves the compiled Workflow script text as a snapshot file (`[x64].txt` or `[x86].txt`) for Workflow generation stability verification.
### Start Functions
- `GacUIUnitTest_Start(appName, config)` — synchronous in-process test with optional serialization channel.
- `GacUIUnitTest_StartAsync(appName, config)` — async test with core and renderer on separate threads.
- `GacUIUnitTest_Start_WithResourceAsText(appName, config, resourceText)` — wraps resource compilation via `GacUIUnitTest_LinkGuiMainProxy`, then delegates to `GacUIUnitTest_Start` or `GacUIUnitTest_StartAsync` depending on `config.useChannel`.
## Protocol Stack Construction
### Synchronous Mode (GacUIUnitTest_Start)
`GacUIUnitTest_Start` constructs the full protocol stack in-process:
**Renderer side (deserialization direction):**
- `UnitTestRemoteProtocol` — mock `IGuiRemoteProtocol` implementation.
- `GuiRemoteJsonChannelFromProtocol` — converts protocol calls to JSON.
- `GuiRemoteJsonChannelStringDeserializer` — JSON to String.
- `GuiRemoteUtfStringChannelDeserializer` — String to UTF-8.
**Core side (serialization direction, mirrors back):**
- `GuiRemoteUtfStringChannelSerializer` — UTF-8 to String.
- `GuiRemoteJsonChannelStringSerializer` — String to JSON.
- `GuiRemoteProtocolFromJsonChannel` — JSON to typed protocol calls.
**Protocol filter layers on core side:**
- `GuiRemoteProtocolFilterVerifier` — validates repeat-filtering invariants.
- `GuiRemoteProtocolFilter` — filters redundant messages.
- `GuiRemoteProtocolDomDiffConverter` — (optional, when `useDomDiff` is true) converts full DOM to DOM diffs.
When `useChannel == UnitTestRemoteChannel::None`, the verifier directly wraps `UnitTestRemoteProtocol`'s `IGuiRemoteProtocol`, bypassing the serialization layers for speed. Otherwise, the full serialization channel is used for testing round-trip fidelity.
Finally, `SetupRemoteNativeController(protocol)` creates the runtime stack: `GuiRemoteController` → `GuiHostedController` → resource managers, then calls `GuiApplicationMain()` → `GuiMain()` → the registered test proxy.
### Async Mode (GacUIUnitTest_StartAsync)
`GacUIUnitTest_StartAsync` inserts `GuiRemoteProtocolAsyncJsonChannelSerializer` into the channel, placing the core and renderer on separate threads. Two threads are spawned via `RunInNewThread`: a channel thread for serialization I/O and a UI thread for the GacUI application. The call waits for `asyncChannelSender.WaitForStopped()` before writing snapshots.
## UnitTestRemoteProtocol Class Hierarchy
`UnitTestRemoteProtocol` inherits from `UnitTestRemoteProtocolFeatures` and `IGuiRemoteEventProcessor`. Its base classes are:
- `UnitTestRemoteProtocolBase` — holds `IGuiRemoteProtocolEvents*`, `UnitTestScreenConfig`, default `Impl_*` stubs, `Initialize`, `GetExecutablePath`.
- `UnitTestRemoteProtocol_MainWindow` — simulates window management (bounds, sizing config, styles).
- `UnitTestRemoteProtocol_IO` — simulates controller connection and IO event forwarding.
- `UnitTestRemoteProtocol_Rendering` — simulates element creation/destruction, rendering begin/end, DOM capture.
- `UnitTestRemoteProtocol_IOCommands` — provides high-level input simulation methods.
- `UnitTestRemoteProtocolFeatures` — combines all feature bases and implements `LogRenderingResult()`, the core frame capture mechanism.
`UnitTestRemoteProtocol` adds:
- `processRemoteEvents` — a list of `(Nullable, Func)` pairs representing named frame callbacks.
- `OnNextIdleFrame(callback)` and `OnNextIdleFrame(name, callback)` — register callbacks to execute after specific rendering frames settle.
- `ProcessRemoteEvents()` — the main event processing method called each cycle.
## The Main Event Loop
`GuiRemoteController::RunOneCycle()` drives the core side per cycle:
1. `remoteProtocol->GetRemoteEventProcessor()->ProcessRemoteEvents()` — this invokes `UnitTestRemoteProtocol::ProcessRemoteEvents()`.
2. `remoteMessages.Submit(disconnected)` — flushes pending protocol messages.
3. `callbackService.InvokeGlobalTimer()` — triggers `GuiHostedController::GlobalTimer()`, which performs layout and rendering, generating new DOM/element data.
4. `asyncService.ExecuteAsyncTasks()`.
The cycle repeats until `connectionStopped` is true (application closes).
## Frame Capture and Snapshot Mechanism
### Passive Rendering Capture (UnitTestRemoteProtocol_Rendering)
Each time the GacUI core calls `RequestRendererBeginRendering`, `UnitTestRemoteProtocol_Rendering::Impl_RendererBeginRendering` creates a new `UnitTestLoggedFrame` and records the `frameId` from the core's monotonic counter. When `Impl_RendererEndRendering` is called, the frame's `renderingDom` is captured from the rendering commands.
### Active Frame Logging (LogRenderingResult)
`LogRenderingResult()` is called at the start of each `ProcessRemoteEvents()` cycle in `UnitTestRemoteProtocolFeatures`:
1. `TryGetLastRenderingFrameAndReset()` checks if a rendering frame completed since the last check. If yes, it becomes the `candidateFrame`.
2. On the next cycle where no new rendering occurred (the UI has settled), the `candidateFrame` is committed to `loggedTrace.frames` with a copy of `lastElementDescs` and `sizingConfig`. This ensures the snapshot captures the final stable rendering state after all layout passes.
3. `LogRenderingResult()` returns `true`, signaling that a frame is ready.
4. In `ProcessRemoteEvents()`, the `frameName` is set on the just-committed frame (from the callback's registered name), and then the callback function executes.
5. If 100 consecutive cycles pass without any rendering change after previously rendering, the test fails with an error.
### Frame Name Semantics
The frame name is assigned to the **already-committed snapshot** (the rendering result), then the callback executes. This means:
- The `frameName` describes what **led to** the current visual state, not what the callback will do next.
- The first frame is conventionally named `"Ready"`, representing the initial rendering state after the window opens.
- Subsequent frame names describe the action taken in the previous callback (e.g., `"Hover"` means the previous callback moved the mouse, and this snapshot shows the hover state).
The sequence is:
1. Application renders → DOM/elements captured as `candidateFrame`.
2. UI settles (no more rendering) → `candidateFrame` committed to `loggedTrace.frames`.
3. `frameName` is set on the committed frame from the callback registration.
4. Frame callback executes (may trigger events → triggering more rendering).
5. Return to step 1.
### Snapshot File Structure
For a test named `Controls/Basic/GuiButton/ClickOnMouseUp`:
- **Main trace file**: `{snapshotFolder}/Controls/Basic/GuiButton/ClickOnMouseUp.json` — `UnitTest_RenderingTrace` with `createdElements`, `imageCreations`, `imageMetadatas`, and `frames` (each frame only has `frameId` and `frameName`, detail stripped).
- **Per-frame files**: `{snapshotFolder}/Controls/Basic/GuiButton/ClickOnMouseUp/frame_0.json`, `frame_1.json`, etc. — full `UnitTest_RenderingFrame` with `frameId`, `frameName`, `windowSize`, `elements` (all rendered element descriptions keyed by ID), and `root` (the rendering DOM tree).
- **Workflow snapshot**: `ClickOnMouseUp[x64].txt` or `[x86].txt` — compiled Workflow script text for generation stability verification.
- **Commands log**: `ClickOnMouseUp[commands].txt` — rendering command logs per frame (when not using DOM diff).
- **Diffs log**: `ClickOnMouseUp[diffs].txt` — DOM diff logs per frame.
### Write Strategy (GacUIUnitTest_LogUI)
`GacUIUnitTest_LogUI` performs the following:
1. Serializes the full `loggedTrace` to JSON and verifies round-trip fidelity (serialize → deserialize → re-serialize must match).
2. Writes each frame as an individual `frame_N.json` file via `GacUIUnitTest_WriteSnapshotFileIfChanged`, which only writes if content differs from the existing file (enabling clean `git diff`).
3. Strips frame detail from the in-memory trace (keeping only `frameId` and `frameName`) and writes the main trace file.
4. Deletes any leftover `frame_*.json` files that no longer correspond to actual frames.
## User Interaction Simulation
### UnitTestRemoteProtocol_IOCommands
`UnitTestRemoteProtocol_IOCommands` provides methods to simulate all user input. It maintains virtual state:
- `mousePosition` — current mouse position (`Nullable`, initially unset; first `MouseMove` triggers `OnIOMouseEntered`).
- `pressingKeys` — `SortedList` of currently pressed keys.
- `leftPressing`, `middlePressing`, `rightPressing` — mouse button states.
- `capslockToggled` — capslock toggle state.
Each method constructs the appropriate `NativeWindowMouseInfo`, `NativeWindowKeyInfo`, or `NativeWindowCharInfo` struct (via `MakeMouseInfo()`, `MakeKeyInfo()`, `MakeCharInfo()`) and calls the corresponding event on `IGuiRemoteProtocolEvents` (obtained via `UseEvents()`).
### Location Calculation (LocationOf)
`LocationOf(controlOrComposition, ratioX, ratioY, offsetX, offsetY)` computes the target point in absolute screen coordinates:
1. Gets the composition's `GetGlobalBounds()` (logical coordinates).
2. Converts to native coordinates using `nativeWindow->Convert()`.
3. Applies the ratio (default 0.5 = center) and offset.
4. Adds the window's screen position `nativeWindow->GetBounds().LeftTop()`.
Overloads accept either `GuiGraphicsComposition*` or `GuiControl*`.
### Mouse Input Methods
- `MouseMove(location)` — sends `OnIOMouseMoving`. If mouse was previously unset, first sends `OnIOMouseEntered`.
- `_LDown(location)` / `_LUp(location)` — low-level left button down/up. `_LDown` calls `MouseMove` if position changed, then `UseEvents().OnIOButtonDown({Left, ...})`.
- `LClick(location)` — `_LDown` followed by `_LUp`.
- `LDBClick(location)` — two `LClick` calls (the framework detects double click from timing).
- Analogous `RClick`, `MClick`, `RDBClick`, `MDBClick` for right and middle buttons.
- `WheelUp(jumps)` / `WheelDown(jumps)` — `OnIOVWheel` with delta scaled by 120 per jump.
- `HWheelLeft(jumps)` / `HWheelRight(jumps)` — `OnIOHWheel` similarly.
### Key and Character Input Methods
- `KeyDown(key)` / `KeyUp(key)` — sends `OnIOKeyDown` / `OnIOKeyUp`. Tracks `pressingKeys` state. Special handling for `VKEY::KEY_CAPITAL` toggles `capslockToggled`.
- `KeyPress(key)` — `KeyDown` followed by `KeyUp`.
- `KeyPress(key, ctrl, shift, alt)` — wraps the key press with modifier key down/up events.
- `TypeString(text)` — sends a sequence of `OnIOChar` events for each character via `MakeCharInfo`, without synthesizing key down/up events.
### Event Flow Through the Pipeline
When a test calls `protocol->LClick(location)`:
1. `_LDown(location)` → `MouseMove(location)` if position changed → `UseEvents().OnIOButtonDown({Left, MakeMouseInfo()})`.
2. `_LUp(location)` → `UseEvents().OnIOButtonUp({Left, MakeMouseInfo()})`.
3. These events reach `GuiRemoteEvents` (the `IGuiRemoteProtocolEvents` implementation in `GuiRemoteController`).
4. `GuiRemoteEvents::OnIOButtonDown()` dispatches to `listener->LeftButtonDown(info)` on `remoteWindow`.
5. `GuiGraphicsHost` receives the call as `INativeWindowListener`.
6. `GuiGraphicsHost::LeftButtonDown()` calls `OnMouseInput()`, which converts native coordinates to logical coordinates, performs hit-testing via `FindVisibleComposition()`, creates `GuiMouseEventArgs`, and dispatches via `RaiseMouseEvent()`.
7. The event bubbles through the composition tree, triggering control-level handlers.
All of this happens **synchronously within a single `ProcessRemoteEvents()` call**, so by the time the frame callback returns, all side effects of the interaction have been processed.
### Blocking Function Caveat
If an IO action triggers a blocking function (like `ShowDialog`), the frame callback would never return. The unit test framework detects this via the `frameExecuting` flag and fails. The solution is to wrap the IO action in `GetApplication()->InvokeInMainThread(window, [=]() { protocol->LClick(location); })`, which defers the action to the next event loop iteration.
## Running Multiple Test Applications
Each call to `GacUIUnitTest_Start` runs one complete application lifecycle (from `GuiMain()` to window close). Typically each `TEST_CASE` calls the full `GacUIUnitTest_SetGuiMainProxy` → `GacUIUnitTest_Start` sequence independently.
Within a single application instance, multiple windows can be created inside frame callbacks using `Value::Create(L"namespace::ClassName")` and shown with `Show()`, `ShowModal()`, or `ShowDialog()`.
`GacUIUnitTest_LinkGuiMainProxy` can be called multiple times before `GacUIUnitTest_Start`, chaining multiple setup layers (e.g., one for resources, one for theme, one for additional initialization).