Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions .cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,20 @@ words:
- sysml
- timeslice
- TOCTOU
- unautomatable
- uncontended
- unioned
- unsupplied
- Unwatch
- unwatch
- Unwatches
- unwatches
- Unwatching
- unwatching
- versionmark
- vstest
- Weasyprint
- Winlogon
- xunit
- Xunit
- yamlfix
Expand Down
9 changes: 8 additions & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,8 @@ jobs:
shell: pwsh
env:
SYSML2WORKBENCH_APP_PATH: ${{ github.workspace }}\publish\win-x64\DemaConsulting.SysML2Workbench.Desktop.exe
SYSML2WORKBENCH_ARTIFACTS_DIR: ${{ github.workspace }}\artifacts
SYSML2WORKBENCH_STARTUP_FILE: ${{ github.workspace }}\test\DemaConsulting.SysML2Workbench.IntegrationTests\TestData\InspectionSample.sysml
run: >
./run-under-appium.ps1 --
dotnet test
Expand Down Expand Up @@ -528,7 +530,12 @@ jobs:
build-docs:
name: Build Documents
runs-on: windows-latest
needs: [build, codeql]
# Must also wait for appium-windows-integration-tests: its trx artifact is the only
# evidence for the Appium-DriveDesktopApplicationEndToEnd/Appium-LocateControlsByAutomationId
# requirements, and without this dependency the two jobs can run concurrently - if this job's
# "Download all job artifacts" step happens to run before that job finishes uploading its
# artifact, those two requirements are silently missing evidence and --enforce fails.
needs: [build, codeql, appium-windows-integration-tests]
permissions:
contents: read

Expand Down
4 changes: 4 additions & 0 deletions build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,13 @@ if ($IntegrationTest) {
if (-not $buildError) {
Write-Host "Running Appium IntegrationTests..."
$env:SYSML2WORKBENCH_APP_PATH = Join-Path (Get-Location) "publish/$rid/$exeName"
$env:SYSML2WORKBENCH_ARTIFACTS_DIR = Join-Path (Get-Location) "artifacts"
$env:SYSML2WORKBENCH_STARTUP_FILE = Join-Path (Get-Location) "test/DemaConsulting.SysML2Workbench.IntegrationTests/TestData/InspectionSample.sysml"
./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
Remove-Item Env:\SYSML2WORKBENCH_ARTIFACTS_DIR -ErrorAction SilentlyContinue
Remove-Item Env:\SYSML2WORKBENCH_STARTUP_FILE -ErrorAction SilentlyContinue
}
}
}
Expand Down
29 changes: 29 additions & 0 deletions docs/design/ots/appium.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,32 @@ part of the sequence instead of duplicating it inline. The cross-platform
`--filter "Category!=Integration"`, since `IntegrationTests`' tests carry
`[Trait("Category", "Integration")]` and require a running Appium server
that those invocations do not start.

### Troubleshooting: "Failed to locate window of the app." on Windows

If every NovaWindows test in `IntegrationTests` fails identically with
`WindowsDriver` throwing `UnknownError: Failed to locate window of the app.`
(surfaced via NovaWindowsDriver's `changeRootElement`), enable the Appium
server's own log output and look for an underlying error such as:

```text
Exception calling "SetFocus" with "0" argument(s): "Target element cannot receive focus."
```

This signature means NovaWindowsDriver *did* find the launched process and
its window, but Windows refused `SetForegroundWindow`/UI Automation
`SetFocus` for it. The most common root cause is that **the interactive
console session driving the test run is locked** - Windows switches to the
secure Winlogon/`LockApp` desktop while locked, which is isolated from
automation and blocks focus/foreground operations for *any* process, not
just the one under test. This is an environment precondition, not a code
regression: it reproduces identically even with no application code from the
current change involved.

`run-under-appium.ps1` includes a Windows-only preflight check that detects
this condition (via `GetForegroundWindow`/`GetWindowThreadProcessId`
resolving to `LockApp`/`LogonUI`) before running the wrapped test command,
and fails fast with an actionable error instead of letting all tests fail
slowly, one at a time, with this cryptic stack trace. The remediation is
simply to **unlock the interactive console/RDP session** and re-run
`-IntegrationTest`; no code change can substitute for this.
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,19 @@ path.
resolve which file it displays without `MainWindowShell` needing to
expose `OpenTabs` internals further.

**CloseAllSourcesAsync**: Closes every workspace source at once.

- *Parameters*: none.
- *Returns*: `Task<WorkspaceSnapshot>` — the freshly resolved and loaded
(empty) workspace snapshot.
- *Postconditions*: `WorkspaceSourceSet.ClearSources` removes every
registered source, then the same watch/unwatch/reload/`SourcesChanged`
reconciliation pipeline used by `AddFileSourceAsync`/`AddFolderSourceAsync`/
`RemoveSourceAsync` is re-run, unwatching every previously watched source
and returning the shell to the exact same valid empty state used at
construction (no sources, no open tabs, no diagnostics). Backs the File
menu's "Close All" command.

**CanExportTabAsSysml**: Reports whether an open diagram tab has a derivable
source definition and can export its diagram as a SysML snippet.

Expand Down Expand Up @@ -259,8 +272,9 @@ the time subscribers observe the notification.

- **WorkspaceSubsystem** — loads and refreshes the workspace state.
- **WorkspaceSourceSet** — owned by the shell; mutated by
`AddFileSourceAsync`/`AddFolderSourceAsync`/`RemoveSourceAsync` and
re-resolved before every `WorkspaceModel` load, reload, and watcher diff.
`AddFileSourceAsync`/`AddFolderSourceAsync`/`RemoveSourceAsync`/
`CloseAllSourcesAsync` and re-resolved before every `WorkspaceModel` load,
reload, and watcher diff.
- **ViewCatalogPresenter** — supplies predefined view choices.
- **ViewDefinitionModel** — captures custom-view authoring state.
- **SysmlSnippetGenerator** — exports custom-view definitions as SysML text.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ itself, a `Folder` source maps to its discovered files).
Removing an unknown id is a no-op that returns `false` rather than
throwing.

**ClearSources**: Removes every registered source at once.

- *Parameters*: `None`.
- *Returns*: `void`.
- *Postconditions*: `Sources` is empty. A no-op (not an error) when already
empty. Backs `MainWindowShell.CloseAllSourcesAsync`, which the File menu's
"Close All" command calls.

**Resolve**: Computes the merged, deduplicated file resolution across every
registered source.

Expand Down
1 change: 1 addition & 0 deletions docs/reqstream/ots/appium.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ sections:
- 'DesktopApp_ViewMenu_ViewBuilderDialogMenuItem_IsDiscoverableAndEnabled'
- 'DesktopApp_QueryMenu_QueryDialogMenuItem_IsDiscoverableAndEnabled'
- 'DesktopApp_HelpMenu_AboutMenuItem_OpensAndClosesAboutDialog'
- 'DesktopApp_FileMenu_CloseAllMenuItem_IsDiscoverableAndEnabled'
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ sections:
- 'RenderCustomViewPreview_DoesNotMutateOpenTabsOrActiveTab'
- 'RenderCustomViewPreview_NoWorkspaceOpened_ThrowsInvalidOperationException'

- id: 'SysML2Workbench-AppShellSubsystem-MainWindowShell-CloseAllSources'
title: 'The MainWindowShell shall close every workspace source at once, returning to the same valid empty state used at construction, and raise SourcesChanged.'
justification: |
The File menu's "Close All" command lets a user reset the entire workspace in one action rather than removing every source individually, and must reach exactly the same fresh-launch empty state (no tabs, no diagnostics, no watched sources) as removing sources one at a time.
tests:
- 'CloseAllSourcesAsync_WithMultipleSources_ProducesEmptySnapshotAndUnwatchesEverything'

- id: 'SysML2Workbench-AppShellSubsystem-MainWindowShell-OpenSourceTextTab'
title: 'The MainWindowShell shall open a read-only source-text tab for a given file path, reusing an already-open tab for the same path instead of duplicating it, and shall resolve an open source-text tab''s file path on request.'
justification: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,22 @@ sections:
- 'ElementFilterViewModel_AddTypeFilter_DuplicateLabel_KeepsSingleChip'
- 'ElementFilterViewModel_RemoveTypeFilter_PresentAndAbsentLabels_BehavesGracefully'
- 'ElementFilterViewModel_GetAddableTypeLabels_ExcludesActiveChips'

- id: 'SysML2Workbench-ElementPickerSubsystem-ElementFilter-SearchAndCommitAddableTypeFilter'
title: 'The ElementFilter shall support a type-ahead search over the addable (not yet
active) type labels: BeginAddableTypeFilterSearch resets the search text and populates
the full addable candidate set, AddableTypeFilterSearchText narrows that set to labels
containing the search text case-insensitively, and TryCommitAddableTypeFilterSearch adds
the top matching label as a new chip (returning true) or leaves the chip set unchanged
(returning false) when no candidate matches.'
justification: |
A pointer click into the "+" add-flyout''s open list races the flyout''s own light-dismiss
overlay under UI-automation drivers (confirmed by direct observation under NovaWindows),
so a type-ahead search box that commits via Enter gives both end users and UI-automation
tests a reliable, keyboard-only way to add a type-filter chip without relying on pointer
hit-testing into a transient popup.
tests:
- 'ElementFilterViewModel_BeginAddableTypeFilterSearch_ResetsSearchAndPopulatesFullSet'
- 'ElementFilterViewModel_AddableTypeFilterSearchText_NarrowsCandidatesCaseInsensitively'
- 'ElementFilterViewModel_TryCommitAddableTypeFilterSearch_MatchFound_AddsChipAndReturnsTrue'
- 'ElementFilterViewModel_TryCommitAddableTypeFilterSearch_NoMatch_ReturnsFalse'
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ sections:
tests:
- 'RemoveSource_RegisteredThenUnknownId_ReturnsTrueThenFalse'

- id: 'SysML2Workbench-WorkspaceSubsystem-WorkspaceSourceSet-ClearSources'
title: 'The WorkspaceSourceSet shall remove every registered source at once.'
justification: |
The File menu's "Close All" command needs a single, no-error way to reset the source set to empty rather than removing each source individually.
tests:
- 'ClearSources_WithRegisteredSources_RemovesAllAndResolveReturnsEmpty'

- id: 'SysML2Workbench-WorkspaceSubsystem-WorkspaceSourceSet-ResolveEmptySet'
title: 'The WorkspaceSourceSet shall resolve a zero-source set to an empty, non-error resolution.'
justification: |
Expand Down
4 changes: 3 additions & 1 deletion docs/user_guide/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ for setup prerequisites.
the **Predefined Views** and **Diagnostics** panels each show a friendly
"workspace is empty" message instead of rendering against nothing, and
any open diagram tabs are closed. This is the same state the application
starts in before any source is added, not an error condition.
starts in before any source is added, not an error condition. Choosing
**File > Close All** does the same thing in one step - it clears every
added source at once, without needing to remove them one at a time.

While a workspace is open, SysML2Workbench watches every added file and
folder independently for external changes. If you edit a `.sysml` file in
Expand Down
36 changes: 33 additions & 3 deletions docs/verification/ots/appium.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ 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.

Several of these tests mutate shared workspace state (adding a real source, or relying on the
`SYSML2WORKBENCH_STARTUP_FILE`-preloaded fixture) against the one `AppFixture`-launched application process shared
across every `[Fact]` in this class (`[Collection("AppFixture")]`). Rather than resetting the workspace before
every test - which would deterministically wipe the `STARTUP_FILE` preload before the one test that depends on it
ever ran, since that preload only happens once at process launch and cannot be redone per test without driving the
very native "Open File" dialog this design avoids - cleanup is owned only by the one state-mutating test itself,
via `AppFixture.CloseAllSources()` (clicks File > Close All through the real accessibility tree) in a `finally`
block. Every other test in this class only asserts menu-item discoverability/enablement, which holds regardless of
whether a source happens to be loaded.

### Test Scenarios

**DesktopApp_Launch_ShowsMainWindowWithExpectedTitle**: Confirms the Appium
Expand All @@ -37,17 +47,37 @@ The item is not actually clicked, since doing so would open the OS-native
**DesktopApp_ViewMenu_PredefinedViewsMenuItem_IsDiscoverableAndEnabled**,
**DesktopApp_ViewMenu_DiagnosticsMenuItem_IsDiscoverableAndEnabled**,
**DesktopApp_ViewMenu_ViewBuilderDialogMenuItem_IsDiscoverableAndEnabled**,
and **DesktopApp_QueryMenu_QueryDialogMenuItem_IsDiscoverableAndEnabled**:
**DesktopApp_QueryMenu_QueryDialogMenuItem_IsDiscoverableAndEnabled**, and
**DesktopApp_FileMenu_CloseAllMenuItem_IsDiscoverableAndEnabled**:
Each opens its respective top-level menu (`File`/`View`/`Query`) and locates
one child menu item by automation id, asserting it is both displayed and
enabled, then closes the menu via Escape without clicking the item. These
extend automation-id coverage breadth-first across every subsystem's
top-level entry point (workspace panel, predefined views, diagnostics,
custom view builder, query dialog) without needing to click through to a
dialog or native OS surface that this tier cannot yet reliably tear down.
custom view builder, query dialog, close-all) without needing to click
through to a dialog or native OS surface that this tier cannot yet reliably
tear down.

**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.

**DesktopApp_QueryDialog_AddTypeFilterButton_CapturesInspectionScreenshot**: Opens the Query dialog and captures a
cropped PNG of the shared `ElementFilterView`'s "+" add-type-filter button (`AddTypeFilterButton`) to
`artifacts/inspection/query-dialog-add-type-filter-button.png` via `InspectionScreenshot.CaptureElement`, then
closes the dialog. Not a pass/fail correctness assertion (only that the button is displayed and the capture
mechanism completes without error) - the actual visual review of the saved image is a human/agent task, since
automatically detecting styling defects is not a suitable CI gate. Honors `SYSML2WORKBENCH_THEME` for which theme
the single capture reflects.

**DesktopApp_QueryDialog_PopulatedWithSourceAndChip_CapturesInspectionScreenshot**: Opens the Query dialog against
a workspace already populated by the `SYSML2WORKBENCH_STARTUP_FILE`-preloaded `TestData/InspectionSample.sysml`
fixture (avoiding the unautomatable native "Open File" dialog), clicks `AddTypeFilterButton`, selects the
"attribute" entry from its flyout `ListBox` to add a real filter chip, then captures the whole filter-row region
(`ElementFilterRoot`) to `artifacts/inspection/query-dialog-populated-with-chip.png` - proving the already-applied
`ElementFilterView.axaml` chip-foreground contrast fix (`Foreground="#212121"` on the chip text and its "X" remove
button) is legible with a real populated chip, not just an empty "+" button. Resets the shared session's workspace
back to empty via `AppFixture.CloseAllSources()` in a `finally` block (see the Verification Approach section
above for why cleanup is owned only by this one test).
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ source produces a valid empty snapshot (zero sources, zero files), clears every
source's file watcher. Verified by
`MainWindowShellTests.RemoveSourceAsync_DownToZeroSources_ProducesEmptySnapshotAndUnwatchesEverything`.

**CloseAllSourcesAsync_WithMultipleSources_ProducesEmptySnapshotAndUnwatchesEverything**: Closing all sources at
once (starting from a folder plus a file source) produces the same valid empty snapshot shape as removing every
source one at a time, unwatches every previously watched source, and raises `SourcesChanged` for every add plus
the close-all. Verified by
`MainWindowShellTests.CloseAllSourcesAsync_WithMultipleSources_ProducesEmptySnapshotAndUnwatchesEverything`.

**SelectPredefinedView_NoWorkspaceOpened_ThrowsInvalidOperationException**: Selecting a predefined view while zero
workspace sources are open throws `InvalidOperationException` rather than rendering against an empty workspace.
Verified by `MainWindowShellTests.SelectPredefinedView_NoWorkspaceOpened_ThrowsInvalidOperationException`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,23 @@ Verified by `ElementFilterViewModelTests.RemoveTypeFilter_PresentAndAbsentLabels
`AvailableTypeLabels` minus every currently-active chip, so the "+" add-flyout never re-offers a
label that is already active. Verified by
`ElementFilterViewModelTests.GetAddableTypeLabels_ExcludesActiveChips`.

**BeginAddableTypeFilterSearch_ResetsSearchAndPopulatesFullSet**: Opening the "+" add-flyout
resets `AddableTypeFilterSearchText` to empty and populates `AddableTypeFilterCandidates` with the
full addable (not yet active) label set. Verified by
`ElementFilterViewModelTests.ElementFilterViewModel_BeginAddableTypeFilterSearch_ResetsSearchAndPopulatesFullSet`.

**AddableTypeFilterSearchText_NarrowsCandidatesCaseInsensitively**: Setting
`AddableTypeFilterSearchText` narrows `AddableTypeFilterCandidates` to only the addable labels whose
text contains the search text, case-insensitively. Verified by
`ElementFilterViewModelTests.ElementFilterViewModel_AddableTypeFilterSearchText_NarrowsCandidatesCaseInsensitively`.

**TryCommitAddableTypeFilterSearch_MatchFound_AddsChipAndReturnsTrue**: When the current search
narrows to at least one addable candidate, `TryCommitAddableTypeFilterSearch()` adds the top
matching label as a new chip in `ActiveTypeFilters` and returns `true`. Verified by
`ElementFilterViewModelTests.ElementFilterViewModel_TryCommitAddableTypeFilterSearch_MatchFound_AddsChipAndReturnsTrue`.

**TryCommitAddableTypeFilterSearch_NoMatch_ReturnsFalse**: When the current search matches no
addable candidate, `TryCommitAddableTypeFilterSearch()` leaves `ActiveTypeFilters` unchanged and
returns `false`. Verified by
`ElementFilterViewModelTests.ElementFilterViewModel_TryCommitAddableTypeFilterSearch_NoMatch_ReturnsFalse`.
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ regardless of kind. Verified by `WorkspaceSourceSetTests.Sources_PreservesRegist
drops it from `Sources`; removing that same id again, or any unknown id, returns `false` without throwing.
Verified by `WorkspaceSourceSetTests.RemoveSource_RegisteredThenUnknownId_ReturnsTrueThenFalse`.

**ClearSources_WithRegisteredSources_RemovesAllAndResolveReturnsEmpty**: Calling `ClearSources` after registering
both a file and a folder source empties `Sources`, and a subsequent `Resolve()` produces empty `MergedFiles` and
attribution maps. Verified by
`WorkspaceSourceSetTests.ClearSources_WithRegisteredSources_RemovesAllAndResolveReturnsEmpty`.

**Resolve_ZeroSources_ReturnsEmptyResolution**: Resolving a `WorkspaceSourceSet` with zero registered sources
produces an empty `MergedFiles` list and empty attribution maps, with no exception thrown. Verified by
`WorkspaceSourceSetTests.Resolve_ZeroSources_ReturnsEmptyResolution`.
Expand Down
Loading
Loading