Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions .github/workflows/ui-tests.yml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Example GitHub Actions Workflow for UI Tests
# NOTE: This workflow requires a self-hosted Windows runner with an active desktop session.
# It is provided as a reference and is disabled by default.
#
# To enable:
# 1. Set up a self-hosted Windows runner with desktop session
# 2. Tag the runner with 'windows-desktop'
# 3. Uncomment this file (remove the .example extension or create .github/workflows/ui-tests.yml)

# name: Windows UI Tests
#
# on:
# push:
# branches: [ main, develop ]
# pull_request:
# branches: [ main, develop ]
#
# jobs:
# ui-tests:
# # IMPORTANT: This requires a self-hosted runner with Windows desktop session
# runs-on: [self-hosted, windows, windows-desktop]
#
# steps:
# - name: Checkout code
# uses: actions/checkout@v4
#
# - name: Setup .NET
# uses: actions/setup-dotnet@v4
# with:
# dotnet-version: '8.0.x'
#
# - name: Restore dependencies
# run: dotnet restore
#
# - name: Build xBeacon.Windows
# run: dotnet build src/xBeacon.Windows/xBeacon.Windows.csproj --configuration Release --no-restore
#
# - name: Run UI Tests
# run: dotnet test tests/xBeacon.Tests.UI/xBeacon.Tests.UI.csproj --configuration Release --no-build --verbosity normal
#
# - name: Cleanup (Kill any lingering processes)
# if: always()
# run: |
# taskkill /F /IM xBeacon.exe /T 2>$null || echo "No xBeacon processes to clean up"
# shell: pwsh

# Alternative: Run UI tests only on-demand
# name: Windows UI Tests (Manual)
#
# on:
# workflow_dispatch: # Manual trigger only
#
# jobs:
# ui-tests:
# runs-on: [self-hosted, windows, windows-desktop]
#
# steps:
# - name: Checkout code
# uses: actions/checkout@v4
#
# - name: Setup .NET
# uses: actions/setup-dotnet@v4
# with:
# dotnet-version: '8.0.x'
#
# - name: Build and Test
# run: |
# dotnet build src/xBeacon.Windows/xBeacon.Windows.csproj
# dotnet test tests/xBeacon.Tests.UI/xBeacon.Tests.UI.csproj --logger "console;verbosity=detailed"
#
# - name: Upload Test Results
# if: always()
# uses: actions/upload-artifact@v4
# with:
# name: ui-test-results
# path: tests/xBeacon.Tests.UI/TestResults/
#
# - name: Cleanup
# if: always()
# run: |
# Get-Process -Name "xBeacon" -ErrorAction SilentlyContinue | Stop-Process -Force
# shell: pwsh
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ Designed for VDI (Virtual Desktop Infrastructure) environments where administrat
- [User Guide](docs/user-guide.md) - Client installation and usage
- [Server Guide](docs/server-guide.md) - Server deployment and configuration
- [Architecture](docs/architecture.md) - System design
- [UI Testing](docs/ui-testing.md) - Windows UI automation tests with FlaUI
- [Release Process](docs/RELEASE_PROCESS.md) - How to create releases
- [ADRs](docs/adr/) - Architecture decisions
- [spec.yaml](spec.yaml) - Single source of truth for all features and components
Expand Down
192 changes: 192 additions & 0 deletions docs/ui-testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# Windows UI Testing with FlaUI

This document describes how to run Windows UI automation tests for xBeacon using FlaUI.

## Overview

xBeacon uses [FlaUI](https://github.com/FlaUI/FlaUI) for Windows desktop UI automation testing. FlaUI is a pure .NET library that provides UI automation capabilities for WPF and WinForms applications without requiring external services like WinAppDriver.

## Prerequisites

- **Windows 11 (21H2+)** - UI tests must run on a Windows environment with a desktop session
- **.NET 8.0 SDK** - Required to build and run the tests
- **xBeacon.Windows built** - The application must be built before running UI tests

## Running UI Tests Locally

### 1. Build the Application

Before running UI tests, you must build the xBeacon.Windows application:

```bash
dotnet build src/xBeacon.Windows/xBeacon.Windows.csproj
```

Or build the entire solution:

```bash
dotnet build xBeacon.sln
```

### 2. Run the UI Tests

Run all UI tests:

```bash
dotnet test tests/xBeacon.Tests.UI/xBeacon.Tests.UI.csproj
```

Run a specific test:

```bash
dotnet test tests/xBeacon.Tests.UI/xBeacon.Tests.UI.csproj --filter "FullyQualifiedName~App_LaunchesSuccessfully"
```

Run with verbose output:

```bash
dotnet test tests/xBeacon.Tests.UI/xBeacon.Tests.UI.csproj --logger "console;verbosity=detailed"
```

## What the Tests Cover

The UI test suite includes several smoke tests:

1. **App Launch Test** - Verifies the application launches successfully and the dashboard opens
2. **Status Indicator Test** - Checks that status indicators render correctly
3. **Notifications List Test** - Ensures the notifications list renders without errors
4. **Header Buttons Test** - Validates that header buttons (Refresh, Close) are accessible
5. **System Information Test** - Confirms system information sections render properly

## UI Test Mode

The application supports a special `--ui-test` flag that enables deterministic behavior for testing:

- **Skips single-instance check** - Allows multiple instances for parallel testing
- **Opens dashboard automatically** - Dashboard window appears on startup
- **Disables polling** - Prevents unpredictable network requests during tests
- **Enables debug mode** - Provides additional logging and diagnostics

When tests run, they automatically pass `--ui-test --debug` flags to the application.

## Test Architecture

### Test Project Structure

```
tests/xBeacon.Tests.UI/
├── Helpers/
│ └── AppTestHelper.cs # Helper class for launching/managing the app
├── DashboardSmokeTests.cs # Main UI smoke tests
└── xBeacon.Tests.UI.csproj # Test project file
```

### Key Components

- **AppTestHelper** - Manages application lifecycle (launch, find windows, close)
- **FlaUI.UIA3** - UI Automation provider for WPF/WinForms
- **xUnit** - Test framework (consistent with existing unit tests)

## Continuous Integration

UI tests require a Windows environment with an active desktop session. They are **not suitable** for standard headless CI runners.

### CI Requirements

For running UI tests in CI:
- Self-hosted Windows runner with Windows 11
- Active desktop session (not running as a service)
- .NET 8.0 SDK installed

### Example GitHub Actions Workflow

```yaml
name: UI Tests

on: [push, pull_request]

jobs:
ui-tests:
runs-on: [self-hosted, windows, desktop] # Self-hosted runner with desktop

steps:
- uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'

- name: Build Application
run: dotnet build src/xBeacon.Windows/xBeacon.Windows.csproj

- name: Run UI Tests
run: dotnet test tests/xBeacon.Tests.UI/xBeacon.Tests.UI.csproj
```

## Troubleshooting

### Tests Can't Find the Application

**Error:** `Could not find xBeacon.exe`

**Solution:** Make sure you've built xBeacon.Windows before running tests:
```bash
dotnet build src/xBeacon.Windows/xBeacon.Windows.csproj
```

### Application Doesn't Close Cleanly

**Issue:** Application process remains after test

**Solution:** Tests automatically attempt graceful shutdown followed by force kill. If processes persist, manually kill them:
```bash
taskkill /F /IM xBeacon.exe
```

### UI Elements Not Found

**Error:** `FindFirstDescendant returned null`

**Possible causes:**
- Application didn't fully load (increase timeout in test)
- AutomationId doesn't match XAML (verify element names)
- Window state is different than expected

**Debug steps:**
1. Run application manually with `--ui-test --debug` flags
2. Use [Accessibility Insights](https://accessibilityinsights.io/) to inspect UI tree
3. Verify AutomationId values in XAML match test expectations

### Tests Fail on CI but Pass Locally

**Common issues:**
- CI runner doesn't have active desktop session
- Different screen resolution/DPI scaling
- Missing Windows features or dependencies

## Best Practices

1. **Keep tests focused** - Test UI behavior, not business logic
2. **Use AutomationId** - Prefer AutomationId over UI element text for stability
3. **Wait appropriately** - Give UI time to load before assertions
4. **Clean up resources** - Always dispose AppTestHelper to close app cleanly
5. **Avoid flakiness** - Don't rely on timing; use proper waits and retries

## Why FlaUI over WinAppDriver?

- **No external service** - Pure .NET library, no separate driver service
- **Simpler setup** - Works with `dotnet test`, no extra installation
- **Better for .NET apps** - Designed specifically for WPF/WinForms
- **Lower maintenance** - Fewer moving parts for contributors

WinAppDriver is still viable if you need:
- WebDriver protocol compatibility
- Cross-language test clients
- Standardized driver approach

## Additional Resources

- [FlaUI Documentation](https://github.com/FlaUI/FlaUI)
- [UI Automation Fundamentals](https://docs.microsoft.com/en-us/windows/win32/winauto/entry-uiauto-win32)
- [Accessibility Insights for Windows](https://accessibilityinsights.io/downloads/)
8 changes: 5 additions & 3 deletions src/xBeacon.Windows/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ static void Main(string[] args)
{
// Parse command line args
bool debugMode = args.Contains("--debug") || args.Contains("-d");
bool uiTestMode = args.Contains("--ui-test");

// Ensure only one instance is running (skip in debug mode)
// Ensure only one instance is running (skip in debug or UI test mode)
// Global\ prefix ensures single instance across ALL user sessions (important for VDI)
const string mutexName = "Global\\xBeacon_SingleInstance";
_mutex = new Mutex(true, mutexName, out bool createdNew);

if (!createdNew && !debugMode)
if (!createdNew && !debugMode && !uiTestMode)
{
// Another instance is already running
MessageBox.Show(
Expand All @@ -37,7 +38,8 @@ static void Main(string[] args)
Application.SetHighDpiMode(HighDpiMode.PerMonitorV2);

// Run the tray application
Application.Run(new TrayApplicationContext(debugMode));
// UI test mode enables debug mode and uses a deterministic config
Application.Run(new TrayApplicationContext(debugMode || uiTestMode, uiTestMode));
}
finally
{
Expand Down
16 changes: 11 additions & 5 deletions src/xBeacon.Windows/TrayApplicationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@ public class TrayApplicationContext : ApplicationContext
// Store recent notifications for the dashboard
private List<Notification> _recentNotifications = new();
private readonly bool _debugMode;
private readonly bool _uiTestMode;

public TrayApplicationContext(bool debugMode = false)
public TrayApplicationContext(bool debugMode = false, bool uiTestMode = false)
{
_debugMode = debugMode;
_uiTestMode = uiTestMode;
EnsureWpfApplication();

// Initialize platform abstractions
Expand Down Expand Up @@ -107,11 +109,15 @@ public TrayApplicationContext(bool debugMode = false)
}

// Start polling and fetch initial session info
_poller.Start();
_ = RefreshDataAsync();
// In UI test mode, we skip polling to keep things deterministic
if (!_uiTestMode)
{
_poller.Start();
_ = RefreshDataAsync();
}

// In debug mode, show dashboard immediately
if (_debugMode)
// In debug or UI test mode, show dashboard immediately
if (_debugMode || _uiTestMode)
{
_dashboard.ShowNearTray();
}
Expand Down
Loading
Loading