diff --git a/.cspell.yaml b/.cspell.yaml
index 7b340c9..225a2a4 100644
--- a/.cspell.yaml
+++ b/.cspell.yaml
@@ -81,6 +81,17 @@ words:
- xshd
- avaloniaedit
- avares
+ - Appium
+ - appium
+ - NovaWindows
+ - WinAppDriver
+ - WebDriver
+ - webdriver
+ - novawindows
+ - atspi
+ - automatable
+ - startable
+ - unvalidated
ignorePaths:
- '**/.git/**'
- '**/node_modules/**'
diff --git a/.github/agents/developer.agent.md b/.github/agents/developer.agent.md
index 5f2b988..0ca5c39 100644
--- a/.github/agents/developer.agent.md
+++ b/.github/agents/developer.agent.md
@@ -24,8 +24,8 @@ Perform software development tasks by determining and applying appropriate stand
4. **Execute work** following standards requirements and quality checks
5. **Formatting**: Run `pwsh ./fix.ps1` to silently apply all
available auto-fixers (dotnet format, markdown, YAML) before committing
-6. **Build and test** (code changes only): Run `pwsh ./build.ps1` and confirm it
- passes - report FAILED if the build or any tests fail
+6. **Build and test** (code changes only): Run `pwsh ./build.ps1 -Build -Test` and
+ confirm it passes - report FAILED if the build or any tests fail
7. **Generate completion report** per the AGENTS.md reporting requirements - save to
`.agent-logs/{agent-name}-{subject}-{unique-id}.md` and return the summary to the caller
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 2d83545..d7c9be9 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -180,6 +180,9 @@ jobs:
--property:Version=${{ inputs.version }}
- name: Test
+ # IntegrationTests' Appium-driven tests are excluded here (Category!=Integration) because no Appium
+ # server is running in this cross-platform matrix job; they run for real only in the dedicated
+ # appium-windows-integration-tests job below, on windows-latest, where an Appium server is started.
run: >
dotnet test
--no-build
@@ -188,6 +191,7 @@ jobs:
--collect "XPlat Code Coverage;Format=opencover"
--logger "trx;LogFilePrefix=${{ matrix.os }}"
--results-directory artifacts
+ --filter "Category!=Integration"
- name: End Sonar Scanner
env:
@@ -347,6 +351,108 @@ jobs:
name: package-msi
path: package/*.msi
+ # Builds and publishes the Desktop application, then drives it end-to-end through a locally
+ # started Appium server and its NovaWindows driver (the maintained successor to the deprecated
+ # WinAppDriver), exercising test/DemaConsulting.SysML2Workbench.IntegrationTests' real Windows-path
+ # tests via run-under-appium.ps1 (the same script a developer uses locally via
+ # `build.ps1 -IntegrationTest`). This job is intentionally windows-latest-only for now: Avalonia's
+ # AutomationPeer also exposes controls to macOS's NSAccessibility (Appium's Mac2 driver) and Linux's
+ # AT-SPI2 (via KDE's selenium-webdriver-at-spi), and both AppFixture.cs and run-under-appium.ps1
+ # already contain correct OS-branching code for all three platforms, but only Windows has a
+ # provisioned CI runner/driver install today. This job is independent of (does not `need`) the build
+ # job above, so it does not block or duplicate that job's cross-platform packaging; it publishes its
+ # own copy of the Desktop app instead of depending on build's artifact, keeping its job graph simple
+ # and self-contained in the same style as the rest of this workflow.
+ appium-windows-integration-tests:
+ name: Appium Windows Integration Tests
+ needs: quality-checks
+ runs-on: windows-latest
+ permissions:
+ contents: read
+
+ steps:
+ # === INSTALL DEPENDENCIES ===
+ # This section installs all required dependencies and tools for the Appium/NovaWindows session.
+ # Downstream projects: Add any additional dependency installations here.
+
+ - name: Checkout
+ uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
+
+ - name: Setup dotnet
+ uses: actions/setup-dotnet@v6
+ with:
+ dotnet-version: 9.x
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v7
+ with:
+ node-version: 24.x
+
+ - name: Install Appium and the NovaWindows driver
+ run: |
+ npm install -g appium
+ appium driver install --source=npm appium-novawindows-driver
+
+ # === BUILD AND PUBLISH THE DESKTOP APPLICATION ===
+ # This section builds the solution and publishes the self-contained Desktop executable that the
+ # Appium session below launches.
+ # Downstream projects: Add any additional build or publish steps here.
+
+ - name: Restore Dependencies
+ run: >
+ dotnet restore
+
+ - name: Build
+ run: >
+ dotnet build
+ --no-restore
+ --configuration Release
+ --property:Version=${{ inputs.version }}
+
+ - name: Publish self-contained build (win-x64)
+ run: >
+ dotnet publish
+ src/DemaConsulting.SysML2Workbench.Desktop/DemaConsulting.SysML2Workbench.Desktop.csproj
+ --configuration Release
+ --runtime win-x64
+ --self-contained true
+ --property:Version=${{ inputs.version }}
+ --property:PublishSingleFile=true
+ --output publish/win-x64
+
+ # === START APPIUM AND RUN THE INTEGRATION TESTS ===
+ # This section delegates to run-under-appium.ps1 (the same wrapper a developer uses locally via
+ # `build.ps1 -IntegrationTest`) to start a local Appium server, run the IntegrationTests project
+ # against the published Desktop application, then stop the server - sharing one tested code path
+ # instead of duplicating the start/poll/stop logic here.
+ # Downstream projects: Add any additional Appium setup or test steps here.
+
+ - name: Run Appium IntegrationTests
+ shell: pwsh
+ env:
+ SYSML2WORKBENCH_APP_PATH: ${{ github.workspace }}\publish\win-x64\DemaConsulting.SysML2Workbench.Desktop.exe
+ run: >
+ ./run-under-appium.ps1 --
+ dotnet test
+ test/DemaConsulting.SysML2Workbench.IntegrationTests/DemaConsulting.SysML2Workbench.IntegrationTests.csproj
+ --configuration Release
+ --property:Version=${{ inputs.version }}
+ --logger "trx;LogFilePrefix=appium-windows"
+ --results-directory artifacts
+
+ # === UPLOAD ARTIFACTS ===
+ # This section uploads the Appium test results for use by downstream jobs.
+ # Downstream projects: Add any additional artifact uploads here.
+
+ - name: Upload Appium test results
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: artifacts-appium-windows-integration-tests
+ path: artifacts/
+
# Runs CodeQL security and quality analysis, gathering results to include
# in the code quality report.
codeql:
diff --git a/.reviewmark.yaml b/.reviewmark.yaml
index 7b55e23..e6c8581 100644
--- a/.reviewmark.yaml
+++ b/.reviewmark.yaml
@@ -310,6 +310,9 @@ reviews:
- src/**/AppShellSubsystem/SourceTextDocumentView.axaml.cs
- test/**/AppShellSubsystem/MainWindowShellTests.cs
- test/**/AppShellSubsystem/SourceTextDocumentViewModelTests.cs
+ - test/**/AppShellSubsystem/MainWindowShellUiTests.cs
+ - test/DemaConsulting.SysML2Workbench.UiTests/GlobalUsings.cs
+ - test/DemaConsulting.SysML2Workbench.UiTests/TestAppBuilder.cs
- id: SysML2Workbench-AppShellSubsystem-WorkspacePanel
title: Review that SysML2Workbench AppShellSubsystem WorkspacePanel Implementation
is Correct
@@ -326,6 +329,7 @@ reviews:
- src/**/AppShellSubsystem/WorkspacePanelToolView.axaml
- src/**/AppShellSubsystem/WorkspacePanelToolView.axaml.cs
- test/**/AppShellSubsystem/WorkspacePanelToolViewModelTests.cs
+ - test/**/AppShellSubsystem/WorkspacePanelUiTests.cs
- id: SysML2Workbench-AppShellSubsystem-AboutDialog
title: Review that SysML2Workbench AppShellSubsystem AboutDialog Implementation
is Correct
@@ -462,3 +466,10 @@ reviews:
- docs/design/ots/avaloniaedit.md
- docs/verification/ots/avaloniaedit.md
- test/OtsSoftwareTests/AvaloniaEditTests.cs
+ - id: OTS-Appium
+ title: Review that Appium Provides Required Functionality
+ paths:
+ - docs/reqstream/ots/appium.yaml
+ - docs/design/ots/appium.md
+ - docs/verification/ots/appium.md
+ - test/DemaConsulting.SysML2Workbench.IntegrationTests/**
diff --git a/AGENTS.md b/AGENTS.md
index 11e614e..3194a6b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -73,7 +73,8 @@ yet cover something.
- **`pip-requirements.txt`** - Python dependencies for yamllint and yamlfix
- **`docs/sysml2/`** - SysML2 architecture model; authoritative source for software structure
- **`fix.ps1`** - Applies all auto-fixers silently (dotnet format, markdown, YAML). Always exits 0.
-- **`build.ps1`** - Builds the solution and runs all tests.
+- **`build.ps1`** - Builds the solution and/or runs its test tiers on demand via
+ switches (`-Build -Test -IntegrationTest -All`); run with no arguments for usage.
# Standards Application (ALL Agents Must Follow)
diff --git a/README.md b/README.md
index b4dd9fc..811bdfd 100644
--- a/README.md
+++ b/README.md
@@ -43,6 +43,12 @@ building a custom view.
(Windows/Linux/macOS entry point).
- `test/DemaConsulting.SysML2Workbench.Tests/` - unit and subsystem-level
tests mirroring `src/`.
+- `test/DemaConsulting.SysML2Workbench.UiTests/` - headless, in-process
+ Avalonia UI tests (view/view-model interaction, no real window).
+- `test/DemaConsulting.SysML2Workbench.IntegrationTests/` - Appium/AT-SPI-driven
+ end-to-end tests against the compiled Desktop application (Windows, macOS,
+ and Linux; CI runs the Windows job today; requires a running automation
+ server - see "Building and Testing" below).
- `test/OtsSoftwareTests/` - integration tests for the off-the-shelf (OTS)
dependencies (SysML2Tools, Rendering, Avalonia, xUnit).
- `docs/` - requirements (`docs/reqstream/`), design (`docs/design/`),
@@ -52,7 +58,24 @@ building a custom view.
## Building and Testing
```powershell
-pwsh ./build.ps1 # restore, build, and run all tests
-pwsh ./fix.ps1 # auto-fix formatting
-pwsh ./lint.ps1 # lint and compliance checks
+pwsh ./build.ps1 -Build -Test # restore, build, and run the unit/headless test suite
+pwsh ./build.ps1 # no switches: prints usage and exits non-zero
+pwsh ./fix.ps1 # auto-fix formatting
+pwsh ./lint.ps1 # lint and compliance checks
```
+
+`build.ps1` accepts combinable switches: `-Build` (restore + build),
+`-Test` (build if needed, then run the unit/headless suite), `-IntegrationTest`
+(build if needed, then run the Appium/AT-SPI suite), and `-All`
+(equivalent to all three). `build.ps1 -Test` and CI's cross-platform build
+job both run `dotnet test --filter "Category!=Integration"`, so
+`test/DemaConsulting.SysML2Workbench.IntegrationTests`' Appium-driven tests
+are excluded from that default test run. That tier runs for real in CI's
+dedicated `appium-windows-integration-tests` job (`windows-latest`), and can
+now also be run locally on Windows, macOS, or Linux via
+`pwsh ./build.ps1 -IntegrationTest`, which delegates to `run-under-appium.ps1`:
+on Windows/macOS it installs Appium and the NovaWindows/Mac2 driver, publishes
+the Desktop application, starts and polls a local Appium server, runs the
+tests, and always stops the server afterward; on Linux it publishes the
+Desktop application and delegates the whole test run to the pre-installed
+`selenium-webdriver-at-spi-run` wrapper (see `docs/design/ots/appium.md`).
diff --git a/SysML2Workbench.slnx b/SysML2Workbench.slnx
index 931d50a..f7208bb 100644
--- a/SysML2Workbench.slnx
+++ b/SysML2Workbench.slnx
@@ -5,6 +5,8 @@
+
+
diff --git a/build.ps1 b/build.ps1
index e47e93c..f1d9e0e 100644
--- a/build.ps1
+++ b/build.ps1
@@ -1,22 +1,117 @@
# build.ps1
#
-# TODO: Update this script to build and test your solution.
-# Replace SysML2Workbench with your actual solution/project name.
+# PURPOSE:
+# Builds the solution and/or runs its test tiers on demand, selected via
+# switch parameters so contributors and agents can run only the tier(s)
+# they need instead of always paying for a full restore+build+test+
+# integration-test cycle.
+#
+# USAGE:
+# ./build.ps1 [-Build] [-Test] [-IntegrationTest] [-All]
+# Run with no switches to print usage and exit non-zero.
+
+param(
+ [switch]$Build,
+ [switch]$Test,
+ [switch]$IntegrationTest,
+ [switch]$All
+)
+
+function Show-Usage {
+ Write-Host "Usage: build.ps1 [-Build] [-Test] [-IntegrationTest] [-All]"
+ Write-Host " -Build Restore and build the solution (Release configuration)"
+ Write-Host " -Test Run the unit/headless test suite (excludes Appium integration tests)"
+ Write-Host " -IntegrationTest Publish the Desktop app and run the Appium/AT-SPI integration tests (Windows/macOS/Linux)"
+ Write-Host " -All Equivalent to -Build -Test -IntegrationTest"
+ Write-Host "Multiple switches may be combined, e.g.: ./build.ps1 -Build -Test"
+}
+
+if ($All) { $Build = $true; $Test = $true; $IntegrationTest = $true }
+
+if (-not ($Build -or $Test -or $IntegrationTest)) {
+ Show-Usage
+ exit 1
+}
$buildError = $false
+$solutionBuilt = $false # tracks whether restore+build already ran this invocation
+
+function Invoke-SolutionBuild {
+ # Shared by -Build, -Test, and -IntegrationTest so each switch is usable
+ # standalone from a clean checkout without requiring -Build in the same
+ # invocation; only runs once per script invocation.
+ if ($script:solutionBuilt) { return $true }
+
+ Write-Host "Restoring dependencies..."
+ dotnet restore
+ if ($LASTEXITCODE -ne 0) { return $false }
+
+ Write-Host "Building..."
+ dotnet build --no-restore --configuration Release
+ if ($LASTEXITCODE -ne 0) { return $false }
+
+ $script:solutionBuilt = $true
+ return $true
+}
+
+if ($Build) {
+ if (-not (Invoke-SolutionBuild)) { $buildError = $true }
+
+ # [PROJECT-SPECIFIC] Add additional build steps here (e.g., packaging, publishing).
+}
+
+if ($Test) {
+ if (-not (Invoke-SolutionBuild)) {
+ $buildError = $true
+ } else {
+ Write-Host "Running tests..."
+ # IntegrationTests' Appium-driven tests are excluded (Category!=Integration) because this
+ # step does not start an Appium server; that tier runs via -IntegrationTest below (locally)
+ # or CI's dedicated appium-windows-integration-tests job. See docs/design/introduction.md
+ # for the full three-(plus-OTS)-tier test strategy.
+ dotnet test --no-build --configuration Release --logger trx --results-directory artifacts/tests --filter "Category!=Integration"
+ if ($LASTEXITCODE -ne 0) { $buildError = $true }
+ }
+}
+
+if ($IntegrationTest) {
+ if (-not (Invoke-SolutionBuild)) {
+ $buildError = $true
+ } else {
+ if ($IsWindows -or $IsMacOS) {
+ Write-Host "Installing Appium..."
+ npm install -g appium
+ if ($LASTEXITCODE -ne 0) { $buildError = $true }
-Write-Host "Restoring dependencies..."
-dotnet restore
-if ($LASTEXITCODE -ne 0) { $buildError = $true }
+ $driverName = $IsWindows ? "appium-novawindows-driver" : "mac2"
+ $driverArgs = $IsWindows ? @("--source=npm", $driverName) : @($driverName)
+ Write-Host "Installing the $driverName driver..."
+ $driverInstallOutput = appium driver install @driverArgs 2>&1 | Out-String
+ Write-Host $driverInstallOutput
+ if ($LASTEXITCODE -ne 0 -and $driverInstallOutput -notmatch "(?i)already installed") {
+ $buildError = $true
+ }
+ } else {
+ Write-Host "Linux: not installing Appium/a driver - selenium-webdriver-at-spi must already be built and installed (see docs/design/ots/appium.md)."
+ }
-Write-Host "Building..."
-dotnet build --no-restore --configuration Release
-if ($LASTEXITCODE -ne 0) { $buildError = $true }
+ if (-not $buildError) {
+ $rid = $IsWindows ? "win-x64" : ($IsMacOS ? ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64 ? "osx-arm64" : "osx-x64") : "linux-x64")
+ $exeName = $IsWindows ? "DemaConsulting.SysML2Workbench.Desktop.exe" : "DemaConsulting.SysML2Workbench.Desktop"
-Write-Host "Running tests..."
-dotnet test --no-build --configuration Release --logger trx --results-directory artifacts/tests
-if ($LASTEXITCODE -ne 0) { $buildError = $true }
+ Write-Host "Publishing self-contained Desktop build ($rid)..."
+ dotnet publish src/DemaConsulting.SysML2Workbench.Desktop/DemaConsulting.SysML2Workbench.Desktop.csproj --configuration Release --runtime $rid --self-contained true --property:PublishSingleFile=true --output publish/$rid
+ if ($LASTEXITCODE -ne 0) { $buildError = $true }
-# [PROJECT-SPECIFIC] Add additional build steps here (e.g., packaging, publishing).
+ if (-not $buildError) {
+ Write-Host "Running Appium IntegrationTests..."
+ $env:SYSML2WORKBENCH_APP_PATH = Join-Path (Get-Location) "publish/$rid/$exeName"
+ ./run-under-appium.ps1 -- dotnet test test/DemaConsulting.SysML2Workbench.IntegrationTests/DemaConsulting.SysML2Workbench.IntegrationTests.csproj --configuration Release --logger "trx;LogFilePrefix=appium-integration" --results-directory artifacts/tests
+ if ($LASTEXITCODE -ne 0) { $buildError = $true }
+ Remove-Item Env:\SYSML2WORKBENCH_APP_PATH -ErrorAction SilentlyContinue
+ }
+ }
+ }
+}
exit ($buildError ? 1 : 0)
diff --git a/docs/build_notes/toolchain_setup.md b/docs/build_notes/toolchain_setup.md
index 45ddf11..becc666 100644
--- a/docs/build_notes/toolchain_setup.md
+++ b/docs/build_notes/toolchain_setup.md
@@ -44,10 +44,10 @@ After installing the toolchain, validate the environment with these commands:
dotnet --info
-pwsh ./build.ps1
+pwsh ./build.ps1 -Build -Test
```
-If `build.ps1` completes successfully, the local machine has the minimum toolchain
+If `build.ps1 -Build -Test` completes successfully, the local machine has the minimum toolchain
needed for the current repository state.
diff --git a/docs/design/introduction.md b/docs/design/introduction.md
index 0d82f6e..f16bfbd 100644
--- a/docs/design/introduction.md
+++ b/docs/design/introduction.md
@@ -33,11 +33,17 @@ OTS items:
- **Avalonia**: integration and usage design (UI framework).
- **AvaloniaEdit**: integration and usage design (read-only syntax-highlighted text editor control).
- **xUnit**: integration and usage design (test framework).
+- **Appium**: integration and usage design (end-to-end desktop UI automation
+ driver for the compiled application; cross-platform in `build.ps1`, but
+ Windows-only in CI today).
Out of scope: no Shared Packages exist in this repository. Design documents
-are not produced for test projects or build pipeline CI configuration, and
-the internal design of OTS items is excluded — only their integration and
-usage within SysML2Workbench is documented.
+are not produced for test projects or build pipeline CI configuration (this
+means no per-project design doc for `test/DemaConsulting.SysML2Workbench.Tests`,
+`UiTests`, `IntegrationTests`, or `OtsSoftwareTests` themselves - the test
+*strategy* they implement is still described below), and the internal design
+of OTS items is excluded — only their integration and usage within
+SysML2Workbench is documented.
## Software Structure
@@ -81,6 +87,46 @@ thin bootstrap/entry-point project with no independent design of its own.
- **QueryDialog** (Unit) — modal query dialog (Browse + Element Query tabs)
- Desktop (platform head / entry point project; not a separately documented unit)
+## Test Strategy
+
+The repository runs four distinct test tiers, each with a different scope and
+dependency boundary:
+
+- **`test/DemaConsulting.SysML2Workbench.Tests`** — unit and subsystem-level
+ xUnit v3 tests mirroring `src/`, with no UI framework dependency.
+- **`test/DemaConsulting.SysML2Workbench.UiTests`** — headless, in-process
+ Avalonia tests (`Avalonia.Headless.XUnit`) exercising this repository's own
+ view/view-model interaction logic (menu command wiring, dialog open/close,
+ panel state) without a visible window.
+- **`test/DemaConsulting.SysML2Workbench.IntegrationTests`** — Appium-driven,
+ black-box, system-level tests that launch the real, compiled
+ `DemaConsulting.SysML2Workbench.Desktop` executable and drive it through
+ its actual accessibility tree, the same way a user would. This tier has no
+ `ProjectReference` to the local UI project (see `docs/design/ots/appium.md`).
+- **`test/OtsSoftwareTests`** — a sibling, not a duplicate, of the above: it
+ qualifies the OTS dependencies themselves (Avalonia, Dock, AvaloniaEdit,
+ SysML2Tools, Rendering) against their own OTS requirements, using the same
+ headless-Avalonia mechanism as `UiTests` but for a different purpose.
+
+Only the Windows/NovaWindows path of `IntegrationTests` runs automatically in
+CI today (`.github/workflows/build.yaml`'s `appium-windows-integration-tests`
+job, `windows-latest`). Avalonia 12.1.0 also exposes controls to macOS's
+NSAccessibility (Appium's Mac2 driver) and Linux's AT-SPI2 (via KDE's
+`selenium-webdriver-at-spi`), and `AppFixture`/`run-under-appium.ps1` contain
+correct OS-branching code for all three platforms, so a developer can run
+this tier locally on macOS or Linux too - but neither has a provisioned CI
+runner today, so only Windows is validated in CI - see
+`docs/design/ots/appium.md` for the integration pattern. `build.ps1 -Test`
+and the cross-platform `build` job's `Test` step both exclude
+`IntegrationTests`' tests carrying the `Integration` trait via
+`--filter "Category!=Integration"`, so adding `IntegrationTests` to
+`SysML2Workbench.slnx` does not break those runs. `build.ps1 -IntegrationTest`
+provides a cross-platform (Windows/macOS/Linux) local equivalent of CI's
+`appium-windows-integration-tests` job, running this tier for real on demand
+outside CI via `run-under-appium.ps1`, which owns the Appium/AT-SPI server's
+lifecycle so `AppFixture` itself only ever connects to an already-running
+server.
+
## Folder Layout
- **src/** - source files and projects
@@ -88,6 +134,9 @@ thin bootstrap/entry-point project with no independent design of its own.
- **DemaConsulting.SysML2Workbench.Desktop/** - desktop platform head (Windows/Linux/macOS entry point)
- **test/** - test projects
- **DemaConsulting.SysML2Workbench.Tests/** - unit and subsystem-level tests mirroring `src/`
+ - **DemaConsulting.SysML2Workbench.UiTests/** - headless, in-process Avalonia UI tests
+ - **DemaConsulting.SysML2Workbench.IntegrationTests/** - Appium-driven end-to-end tests against the compiled Desktop application
+ - **OtsSoftwareTests/** - integration tests qualifying the OTS dependencies themselves
## Companion Artifact Structure
diff --git a/docs/design/ots/appium.md b/docs/design/ots/appium.md
new file mode 100644
index 0000000..efcf909
--- /dev/null
+++ b/docs/design/ots/appium.md
@@ -0,0 +1,103 @@
+## Appium
+
+SysML2Workbench uses Appium's WebDriver client, together with Appium's
+NovaWindows and Mac2 drivers (and KDE's `selenium-webdriver-at-spi` on Linux),
+to drive the compiled desktop application end-to-end through its real
+accessibility tree as a system-level UI automation test tier.
+
+### Purpose
+
+Appium was chosen because it exercises the actual compiled
+`DemaConsulting.SysML2Workbench.Desktop` application in a real window, driven
+the same way a user would interact with it, complementing the in-process
+`test/DemaConsulting.SysML2Workbench.UiTests` headless tier and the
+`test/DemaConsulting.SysML2Workbench.Tests` unit/subsystem tier. Avalonia's
+`AutomationPeer` exposes controls to UIA (Windows), NSAccessibility
+(macOS, via Appium's Mac2 driver), and AT-SPI2 (Linux, via Avalonia's X11
+backend only - Wayland support is unconfirmed), so Appium can, in principle,
+drive the same application on all three desktop platforms through a common
+`AutomationProperties.AutomationId`-based control lookup.
+
+### Features Used
+
+- **`Appium.WebDriver`'s `WindowsDriver`/`MacDriver`** — connect to a local
+ Appium server running the NovaWindows driver (the maintained successor to
+ the deprecated WinAppDriver) or the Mac2 driver, and launch the published
+ Desktop executable as the driven application. Linux has no dedicated
+ first-party Appium .NET client type (KDE's `selenium-webdriver-at-spi` is
+ not one of Appium's officially-known platforms), so `AppFixture` defines a
+ minimal `LinuxDriver : AppiumDriver` subclass purely to reach an
+ otherwise-inaccessible base constructor.
+- **`AutomationProperties.AutomationId`** — added to `MainWindowView.axaml`'s
+ menu items and dock control, and to the interactive controls of the About
+ dialog, Workspace panel, and Predefined Views panel, giving
+ `MobileBy.AccessibilityId` lookups a stable, Appium-reliable target
+ independent of `Name`/`x:Name`.
+- **`MobileBy.Name`/`MobileBy.AccessibilityId`** — locate top-level menus and
+ automation-id-tagged controls in the real accessibility tree.
+
+### Integration Pattern
+
+`test/DemaConsulting.SysML2Workbench.IntegrationTests` has no `ProjectReference`
+to the local UI project: it drives the compiled application externally as a
+black-box system-level tier, per `testing-principles.md`'s hierarchy-boundary
+rule. `AppFixture` follows Avalonia's documented Appium `AppFixture` pattern,
+branching by `OperatingSystem.IsWindows()`/`IsMacOS()`/`IsLinux()` to select
+the right driver/capabilities, but it never starts, stops, or otherwise
+manages an Appium/AT-SPI server process itself - it always just connects a
+WebDriver client to `http://127.0.0.1:4723`, trusting that something already
+made a server available there before the test process launched. Only the
+Windows/NovaWindows branch is exercised by CI today and validated against a
+real Appium server; the macOS/Mac2 and Linux/AT-SPI2 branches are implemented
+from documentation so a developer can run this tier locally, but neither has
+a provisioned CI runner nor has been exercised against real hardware - treat
+both as best-effort and unvalidated until proven otherwise.
+
+That server's lifecycle is owned entirely by **`run-under-appium.ps1`**
+(repository root), invoked by `build.ps1 -IntegrationTest` and by
+`.github/workflows/build.yaml`'s `appium-windows-integration-tests` job. It
+wraps an arbitrary command (typically `dotnet test ... IntegrationTests.csproj`)
+differently per OS:
+
+- **Windows/macOS**: starts a local Appium server itself (resolving `node`/
+ `appium` directly - on Windows via `node.exe ` to avoid
+ `Start-Process`'s inability to exec a `.cmd` wrapper without breaking PID
+ tracking; on macOS `appium` is a real shebang script, so no such workaround
+ is needed), polls `/status` until ready, runs the wrapped command, then
+ stops the server in a `finally` block regardless of outcome.
+- **Linux**: this is architecturally inverted rather than merely
+ unimplemented. KDE's `selenium-webdriver-at-spi` has no standalone,
+ directly-startable server binary or `--port` flag - its only supported
+ entry point is `selenium-webdriver-at-spi-run `, a wrapper that
+ itself boots a nested Wayland compositor session plus its own Flask/AT-SPI2
+ server, runs the wrapped command as its *child*, and tears everything down
+ together once that child exits. So on Linux, `run-under-appium.ps1` simply
+ delegates the entire wrapped command to that external tool instead of
+ managing anything itself; `build.ps1 -IntegrationTest` does not install or
+ build it - a Linux user is expected to have already built and installed it
+ themselves (see KDE's docs at ) before
+ running `-IntegrationTest` there.
+
+`build.ps1 -IntegrationTest` is cross-platform (Windows/macOS/Linux): on
+Windows/macOS it installs Appium and the appropriate driver (NovaWindows via
+`--source=npm`, or Mac2) via `npm`/`appium driver install`, skipping that step
+entirely on Linux; then it publishes the Desktop application for the current
+OS/architecture's RID and runs `./run-under-appium.ps1 -- dotnet test ...`.
+Only the Windows path is exercised in CI and treated as validated; macOS and
+Linux are provided for developers who want to run this tier locally on those
+platforms.
+
+`.github/workflows/build.yaml`'s `appium-windows-integration-tests` job is the
+only CI job that runs this tier, and remains Windows-only (`windows-latest`)
+even though `build.ps1 -IntegrationTest` itself is now cross-platform - no
+macOS/Linux CI runner is provisioned. It installs Appium and the NovaWindows
+driver via npm, restores/builds/publishes the Desktop application as its own
+separate steps (so each reports its own pass/fail status independently in the
+GitHub Actions UI), then delegates to the same `run-under-appium.ps1` script
+`build.ps1 -IntegrationTest` uses locally to start Appium, run
+`IntegrationTests`, and stop Appium - sharing one tested code path for that
+part of the sequence instead of duplicating it inline. The cross-platform
+`build` job's `Test` step and `build.ps1 -Test` both exclude this tier with
+`--filter "Category!=Integration"`, since `IntegrationTests`' tests carry
+`[Trait("Category", "Integration")]` and require a running Appium server
+that those invocations do not start.
diff --git a/docs/reqstream/ots/appium.yaml b/docs/reqstream/ots/appium.yaml
new file mode 100644
index 0000000..336a782
--- /dev/null
+++ b/docs/reqstream/ots/appium.yaml
@@ -0,0 +1,20 @@
+---
+sections:
+ - title: 'OTS Software Requirements'
+ sections:
+ - title: 'Appium Requirements'
+ requirements:
+ - id: 'Appium-DriveDesktopApplicationEndToEnd'
+ title: 'The SysML2Workbench repository shall use Appium to drive the compiled Desktop application end-to-end through its real accessibility tree on Windows.'
+ justification: |
+ A system-level UI automation tier is needed that exercises the actual compiled application in a real window, complementing the headless and unit test tiers, and Windows is the only platform with a provisioned CI Appium driver today.
+ tests:
+ - 'DesktopApp_Launch_ShowsMainWindowWithExpectedTitle'
+
+ - id: 'Appium-LocateControlsByAutomationId'
+ title: 'The SysML2Workbench repository shall expose interactive controls to Appium via AutomationProperties.AutomationId for reliable, name-independent lookup.'
+ justification: |
+ Appium's accessibility-tree-based lookups need a stable identifier that does not depend on a control''s displayed Name/x:Name, so tests remain reliable as UI text changes.
+ tests:
+ - 'DesktopApp_FileMenu_AddFileSourceMenuItem_IsDiscoverableAndEnabled'
+ - 'DesktopApp_HelpMenu_AboutMenuItem_OpensAndClosesAboutDialog'
diff --git a/docs/sysml2/model/ots.sysml b/docs/sysml2/model/ots.sysml
index e6dfdd2..2cf6b99 100644
--- a/docs/sysml2/model/ots.sysml
+++ b/docs/sysml2/model/ots.sysml
@@ -49,5 +49,12 @@ package SysML2WorkbenchArchitecture {
comment verificationRef /* Verification: docs/verification/ots/avaloniaedit.md */
comment reqRef /* Requirements: docs/reqstream/ots/avaloniaedit.yaml */
}
+ part def Appium {
+ doc /* Appium.WebDriver NuGet dependency, together with Appium's NovaWindows driver, used to drive the compiled Desktop application end-to-end through its real accessibility tree from test/DemaConsulting.SysML2Workbench.IntegrationTests. */
+
+ comment designRef /* Design: docs/design/ots/appium.md */
+ comment verificationRef /* Verification: docs/verification/ots/appium.md */
+ comment reqRef /* Requirements: docs/reqstream/ots/appium.yaml */
+ }
}
}
diff --git a/docs/verification/ots/appium.md b/docs/verification/ots/appium.md
new file mode 100644
index 0000000..6bd493f
--- /dev/null
+++ b/docs/verification/ots/appium.md
@@ -0,0 +1,39 @@
+## Appium
+
+### Verification Approach
+
+Integration tests in `test/DemaConsulting.SysML2Workbench.IntegrationTests/MainWindowShellIntegrationTests.cs`
+qualify the Windows/NovaWindows Appium session by launching the real,
+published `DemaConsulting.SysML2Workbench.Desktop` executable through
+`AppFixture` and driving it through its actual accessibility tree, rather
+than a mocked or headless UI harness, because the end-to-end platform
+integration itself is what this tier is qualifying. These tests only run for
+real in `.github/workflows/build.yaml`'s `appium-windows-integration-tests`
+job (and locally via `build.ps1 -IntegrationTest`/`run-under-appium.ps1`,
+which is cross-platform but only validated on Windows), where an Appium
+server and the NovaWindows driver are actually running; elsewhere (the
+cross-platform `build` job's `Test` step and `build.ps1 -Test`) they are
+excluded via `--filter "Category!=Integration"`, since no Appium/AT-SPI
+server is started in those runs.
+
+### Test Scenarios
+
+**DesktopApp_Launch_ShowsMainWindowWithExpectedTitle**: Confirms the Appium
+session's `Title` reports "SysML2Workbench" immediately after launch, proving
+the session is genuinely driving the compiled application's real main
+window rather than a stub.
+
+**DesktopApp_FileMenu_AddFileSourceMenuItem_IsDiscoverableAndEnabled**:
+Opens the File menu (`MobileBy.Name("File")`) and locates the "Open File..."
+item by its `AddFileSourceMenuItem` automation id
+(`MobileBy.AccessibilityId`), proving Avalonia's UIA automation peer exposes
+the `AutomationProperties.AutomationId` values added to `MainWindowView.axaml`
+through the real accessibility tree that Appium's NovaWindows driver reads.
+The item is not actually clicked, since doing so would open the OS-native
+"Open File" dialog, which lives outside Avalonia's own accessibility tree.
+
+**DesktopApp_HelpMenu_AboutMenuItem_OpensAndClosesAboutDialog**: Opens the
+Help menu, clicks "About" (`AboutMenuItem`), confirms the modal About
+dialog's `AboutDialogOkButton` becomes visible, then dismisses it - proving
+a full menu-click-to-modal-dialog round trip works end-to-end through the
+real windowed application.
diff --git a/requirements.yaml b/requirements.yaml
index 5936723..12c37ce 100644
--- a/requirements.yaml
+++ b/requirements.yaml
@@ -33,3 +33,4 @@ includes:
- docs/reqstream/ots/avalonia.yaml
- docs/reqstream/ots/xunit.yaml
- docs/reqstream/ots/avaloniaedit.yaml
+ - docs/reqstream/ots/appium.yaml
diff --git a/run-under-appium.ps1 b/run-under-appium.ps1
new file mode 100644
index 0000000..5224b28
--- /dev/null
+++ b/run-under-appium.ps1
@@ -0,0 +1,112 @@
+# run-under-appium.ps1
+#
+# PURPOSE:
+# Wraps an arbitrary command (typically `dotnet test` against
+# DemaConsulting.SysML2Workbench.IntegrationTests) with whatever local
+# automation server AppFixture.cs expects to already be listening at
+# http://127.0.0.1:4723 before the wrapped command's process starts:
+# - Windows/macOS: starts a local Appium server (NovaWindows/Mac2 driver
+# is selected inside AppFixture.cs, not here), runs the wrapped
+# command, then stops the server - regardless of whether the wrapped
+# command succeeded, failed, or threw.
+# - Linux: has no directly-startable server binary to manage. KDE's
+# selenium-webdriver-at-spi only supports its own `-run` wrapper,
+# which itself wraps the whole command (boots a nested Wayland
+# session, starts its Flask/AT-SPI2 server, runs the wrapped command
+# as its child, tears everything down together). This script simply
+# delegates to that wrapper instead of managing anything itself.
+#
+# AppFixture.cs never starts/stops a server itself on any OS - it always
+# just connects to http://127.0.0.1:4723, assuming this script (or a
+# developer running the equivalent steps by hand) already made one
+# available.
+#
+# USAGE:
+# ./run-under-appium.ps1 -- [args...]
+# e.g.: ./run-under-appium.ps1 -- dotnet test test/.../DemaConsulting.SysML2Workbench.IntegrationTests.csproj
+#
+# The exit code of the wrapped command is propagated as this script's
+# exit code.
+
+param(
+ [Parameter(ValueFromRemainingArguments = $true)]
+ [string[]]$Command
+)
+
+if (-not $Command -or $Command.Count -eq 0) {
+ Write-Error "Usage: run-under-appium.ps1 -- [args...]"
+ exit 1
+}
+
+$commandExe = $Command[0]
+$commandArgs = if ($Command.Count -gt 1) { $Command[1..($Command.Count - 1)] } else { @() }
+
+if ($IsLinux) {
+ $atSpiRun = Get-Command selenium-webdriver-at-spi-run -ErrorAction SilentlyContinue
+ if (-not $atSpiRun) {
+ Write-Error "selenium-webdriver-at-spi-run was not found on PATH. Build and install it from https://invent.kde.org/sdk/selenium-webdriver-at-spi (see docs/design/ots/appium.md), then re-run."
+ exit 1
+ }
+
+ # selenium-webdriver-at-spi-run boots its own nested Wayland session and
+ # AT-SPI2/WebDriver server, runs the wrapped command as its child, and
+ # tears everything down together - so it owns the entire lifecycle here;
+ # this script does not poll/start/stop anything itself on Linux.
+ Write-Host "Running under selenium-webdriver-at-spi-run: $commandExe $($commandArgs -join ' ')"
+ & selenium-webdriver-at-spi-run $commandExe @commandArgs
+ exit $LASTEXITCODE
+}
+
+# Windows/macOS: this script owns the Appium server's lifecycle itself via
+# AppiumServiceBuilder-equivalent plain node/appium spawning, since
+# AppFixture.cs never starts one.
+$appiumProcess = $null
+$exitCode = 1
+try {
+ if ($IsWindows) {
+ # appium is installed as appium.cmd on Windows, which Start-Process
+ # cannot exec directly (no shebang support); going through cmd.exe
+ # would break PID tracking for the Stop-Process call below. Resolve
+ # and launch the real JS entry point via node.exe instead, bypassing
+ # the .cmd wrapper entirely.
+ $appiumCmd = Get-Command appium.cmd -ErrorAction Stop
+ $appiumEntry = Join-Path (Split-Path $appiumCmd.Source -Parent) "node_modules\appium\index.js"
+ if (-not (Test-Path $appiumEntry)) {
+ throw "Could not locate the Appium entry point at '$appiumEntry'."
+ }
+
+ Write-Host "Starting Appium server..."
+ $appiumProcess = Start-Process -FilePath "node" -ArgumentList $appiumEntry, "--base-path", "/", "--allow-insecure", "*:chromedriver_autodownload" -WindowStyle Hidden -PassThru
+ } else {
+ # macOS: appium is a real shebang script on PATH, so it can be
+ # exec'd directly - no .cmd/index.js resolution dance needed.
+ Write-Host "Starting Appium server..."
+ $appiumProcess = Start-Process -FilePath "appium" -ArgumentList "--base-path", "/", "--allow-insecure", "*:chromedriver_autodownload" -PassThru
+ }
+
+ $ready = $false
+ for ($i = 0; $i -lt 30; $i++) {
+ try {
+ $response = Invoke-WebRequest -Uri "http://127.0.0.1:4723/status" -UseBasicParsing -TimeoutSec 2
+ if ($response.StatusCode -eq 200) { $ready = $true; break }
+ } catch {
+ Start-Sleep -Seconds 2
+ }
+ }
+
+ if (-not $ready) {
+ Write-Error "Appium server did not become ready within the expected time."
+ exit 1
+ }
+
+ Write-Host "Running: $commandExe $($commandArgs -join ' ')"
+ & $commandExe @commandArgs
+ $exitCode = $LASTEXITCODE
+} finally {
+ if ($appiumProcess -and -not $appiumProcess.HasExited) {
+ Write-Host "Stopping Appium server..."
+ Stop-Process -Id $appiumProcess.Id -Force -ErrorAction SilentlyContinue
+ }
+}
+
+exit $exitCode
diff --git a/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/AboutDialogView.axaml b/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/AboutDialogView.axaml
index 004e03d..1d23440 100644
--- a/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/AboutDialogView.axaml
+++ b/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/AboutDialogView.axaml
@@ -12,7 +12,7 @@
-
+
diff --git a/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/MainWindowView.axaml b/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/MainWindowView.axaml
index 446e08d..049f03f 100644
--- a/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/MainWindowView.axaml
+++ b/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/MainWindowView.axaml
@@ -12,55 +12,55 @@
-
+
+
+
diff --git a/test/DemaConsulting.SysML2Workbench.IntegrationTests/GlobalUsings.cs b/test/DemaConsulting.SysML2Workbench.IntegrationTests/GlobalUsings.cs
new file mode 100644
index 0000000..c802f44
--- /dev/null
+++ b/test/DemaConsulting.SysML2Workbench.IntegrationTests/GlobalUsings.cs
@@ -0,0 +1 @@
+global using Xunit;
diff --git a/test/DemaConsulting.SysML2Workbench.IntegrationTests/MainWindowShellIntegrationTests.cs b/test/DemaConsulting.SysML2Workbench.IntegrationTests/MainWindowShellIntegrationTests.cs
new file mode 100644
index 0000000..791afc2
--- /dev/null
+++ b/test/DemaConsulting.SysML2Workbench.IntegrationTests/MainWindowShellIntegrationTests.cs
@@ -0,0 +1,94 @@
+using OpenQA.Selenium.Appium;
+
+namespace DemaConsulting.SysML2Workbench.IntegrationTests;
+
+///
+/// Real Appium tests driving the compiled DemaConsulting.SysML2Workbench.Desktop application through
+/// the -launched session (NovaWindows on Windows, Mac2 on macOS, AT-SPI2 on Linux),
+/// using the AutomationProperties.AutomationId values added to MainWindowView.axaml. Marked
+/// [Trait("Category", "Integration")] so .github/workflows/build.yaml's cross-platform
+/// build job (which has no Appium server running) can exclude this tier via
+/// --filter "Category!=Integration", while the dedicated appium-windows-integration-tests job
+/// (and a developer running build.ps1 -IntegrationTest locally on any OS) runs it for real.
+///
+[Collection("AppFixture")]
+[Trait("Category", "Integration")]
+public sealed class MainWindowShellIntegrationTests
+{
+ private readonly AppiumDriver _session;
+
+ ///
+ /// Receives the assembly-shared 's launched application session.
+ ///
+ /// The shared fixture that launched the Desktop application.
+ public MainWindowShellIntegrationTests(AppFixture fixture)
+ {
+ _session = fixture.Driver;
+ }
+
+ ///
+ /// Validates that launching the real Desktop application presents its main window titled
+ /// "SysML2Workbench", proving the Appium session is actually driving the compiled application rather
+ /// than a stub, and that the window title test/DemaConsulting.SysML2Workbench.UiTests's
+ /// MainWindowView_Startup_HasSysML2WorkbenchTitle test asserts in-process also holds true when the
+ /// real windowed application is launched end-to-end.
+ ///
+ [Fact]
+ public void DesktopApp_Launch_ShowsMainWindowWithExpectedTitle()
+ {
+ // Arrange / Act
+ var title = _session.Title;
+
+ // Assert
+ Assert.Equal("SysML2Workbench", title);
+ }
+
+ ///
+ /// Validates that the File menu's "Open File..." item, found by the AddFileSourceMenuItem
+ /// automation id added to MainWindowView.axaml, is discoverable and enabled through the real
+ /// accessibility tree exposed by Avalonia's UIA automation peer. The menu item is deliberately not
+ /// clicked here: doing so would open the OS-native "Open File" dialog, which lives outside Avalonia's
+ /// own accessibility tree and is not reliably automatable through the same driver session across
+ /// Windows/macOS/Linux.
+ ///
+ [Fact]
+ public void DesktopApp_FileMenu_AddFileSourceMenuItem_IsDiscoverableAndEnabled()
+ {
+ // Arrange
+ var fileMenu = _session.FindElement(MobileBy.Name("File"));
+ fileMenu.Click();
+
+ // Act
+ var addFileMenuItem = _session.FindElement(MobileBy.AccessibilityId("AddFileSourceMenuItem"));
+
+ // Assert
+ Assert.True(addFileMenuItem.Displayed);
+ Assert.True(addFileMenuItem.Enabled);
+
+ // Close the menu without picking a file so this test leaves no dialog open behind it.
+ addFileMenuItem.SendKeys(OpenQA.Selenium.Keys.Escape);
+ }
+
+ ///
+ /// Validates that clicking the Help menu's "About" item, found by the AboutMenuItem automation
+ /// id, opens the modal About dialog, discoverable by its own AboutDialogOkButton automation id,
+ /// and that dismissing it via that button closes the dialog again.
+ ///
+ [Fact]
+ public void DesktopApp_HelpMenu_AboutMenuItem_OpensAndClosesAboutDialog()
+ {
+ // Arrange
+ var helpMenu = _session.FindElement(MobileBy.Name("Help"));
+ helpMenu.Click();
+ var aboutMenuItem = _session.FindElement(MobileBy.AccessibilityId("AboutMenuItem"));
+
+ // Act
+ aboutMenuItem.Click();
+ var okButton = _session.FindElement(MobileBy.AccessibilityId("AboutDialogOkButton"));
+
+ // Assert
+ Assert.True(okButton.Displayed);
+
+ okButton.Click();
+ }
+}
diff --git a/test/DemaConsulting.SysML2Workbench.UiTests/AppShellSubsystem/MainWindowShellUiTests.cs b/test/DemaConsulting.SysML2Workbench.UiTests/AppShellSubsystem/MainWindowShellUiTests.cs
new file mode 100644
index 0000000..52584a1
--- /dev/null
+++ b/test/DemaConsulting.SysML2Workbench.UiTests/AppShellSubsystem/MainWindowShellUiTests.cs
@@ -0,0 +1,91 @@
+using Avalonia.Controls;
+using Avalonia.Headless.XUnit;
+using Avalonia.Threading;
+using DemaConsulting.SysML2Workbench.AppShellSubsystem;
+using DemaConsulting.SysML2Workbench.DiagnosticsPanelSubsystem;
+using DemaConsulting.SysML2Workbench.LayoutRenderingSubsystem;
+using DemaConsulting.SysML2Workbench.LoggingSubsystem;
+using DemaConsulting.SysML2Workbench.ViewBuilderSubsystem;
+using DemaConsulting.SysML2Workbench.ViewCatalogSubsystem;
+using DemaConsulting.SysML2Workbench.WorkspaceSubsystem;
+
+namespace DemaConsulting.SysML2Workbench.UiTests.AppShellSubsystem;
+
+///
+/// Local view/view-model interaction tests for , running under Avalonia's
+/// headless test platform (). Unlike
+/// test/OtsSoftwareTests/AvaloniaTests.cs, which qualifies the OTS Avalonia dependency itself against
+/// its own requirements, these tests exercise this repository's own menu-command wiring and window
+/// composition logic in-process, with no real window/Appium session involved.
+///
+public sealed class MainWindowShellUiTests : IDisposable
+{
+ private readonly string _tempLogRoot = Directory.CreateTempSubdirectory("sysml2workbench-ui-tests-logs-").FullName;
+
+ ///
+ public void Dispose()
+ {
+ if (Directory.Exists(_tempLogRoot))
+ {
+ Directory.Delete(_tempLogRoot, recursive: true);
+ }
+ }
+
+ private MainWindowShell CreateShell()
+ {
+ return new MainWindowShell(
+ new WorkspaceModel(),
+ new FileWatcher(TimeSpan.FromMilliseconds(1)),
+ new DiagnosticsAggregator(),
+ new ViewCatalogPresenter(),
+ new LayoutInvoker(),
+ new DiagnosticsListView(),
+ new SysmlSnippetGenerator(),
+ new RollingFileLogger(_tempLogRoot));
+ }
+
+ ///
+ /// Validates that the main window is constructed with the application's fixed title, since Appium's
+ /// Windows-path tests in test/DemaConsulting.SysML2Workbench.IntegrationTests rely on this exact
+ /// title to find and assert against the real launched window.
+ ///
+ [AvaloniaFact]
+ public void MainWindowView_Startup_HasSysML2WorkbenchTitle()
+ {
+ // Arrange
+ using var shell = CreateShell();
+
+ // Act
+ var window = new MainWindowView(shell);
+
+ // Assert
+ Assert.Equal("SysML2Workbench", window.Title);
+ }
+
+ ///
+ /// Validates that clicking the File menu's "Exit" item closes the main window, proving the click handler
+ /// wired in MainWindowView.axaml.cs (OnExitMenuItemClick) actually invokes
+ /// rather than merely being present in the XAML.
+ ///
+ [AvaloniaFact]
+ public void MainWindowView_ExitMenuItem_Click_ClosesWindow()
+ {
+ // Arrange
+ using var shell = CreateShell();
+ var window = new MainWindowView(shell);
+ window.Show();
+ Dispatcher.UIThread.RunJobs();
+ var exitMenuItem = window.FindControl