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 @@ -