Skip to content

Add AddResourceDictionary API and cross-app testing sample#135

Open
mattleibow wants to merge 19 commits into
mainfrom
mattleibow/visual-runner-as-service
Open

Add AddResourceDictionary API and cross-app testing sample#135
mattleibow wants to merge 19 commits into
mainfrom
mattleibow/visual-runner-as-service

Conversation

@mattleibow
Copy link
Copy Markdown
Owner

@mattleibow mattleibow commented May 21, 2026

Summary

Enables testing custom controls, ViewModels, and services inside a real MAUI app by referencing the app as a library from a separate test project. The app's styles, DI container, and resources are fully available in tests.

Key changes

  1. AddResourceDictionary<T>() API — Register app resource dictionaries (Colors, Styles) into the visual test runner so {StaticResource} resolves correctly during tests.

  2. Page-level runner resources — Runner styles moved from Application.Resources to page-level VisualRunnerResources.xaml, preventing conflicts with app styles.

  3. Library-mode project reference — Test project references the app with <AdditionalProperties>Configuration=Library$(Configuration)</AdditionalProperties>, which triggers the app-side targets to build as a library (no entry points, no icons, no packaging).

  4. TestingWorkarounds.targets (drop-in files) — Two .targets files handle all MSBuild workarounds:

    • App-side: Detects library mode, sets OutputType=Library, strips platform entry points, removes icons/splash, disables packaging. Propagates Configuration to transitive refs to prevent parallel build races.
    • Test-side: Strips leaked Resizetizer items and fixes Windows PRI generation for the referenced app.
  5. Sample DeviceTestingKitApp.AppTests — 10 NUnit tests demonstrating the pattern: page creation with styles, binding context, custom controls, command wiring, live modal navigation.

  6. Documentation — Full guide at docs/articles/testing-with-app-resources.md with drop-in code fences for both targets files.

Upstream issues filed

Architecture

Test App (AppTests)
  └── ProjectReference: DeviceTestingKitApp (Configuration=LibraryRelease)
        ├── OutputType=Library (no entry point)
        ├── MauiIcon/SplashScreen removed
        ├── Platform entry points stripped
        └── ProjectReference: MauiLibrary (Configuration=LibraryRelease)
              └── Separate intermediate paths (no parallel race)

What users do

  1. Add TestingWorkarounds.targets to their app (imports via csproj)
  2. Add TestingWorkarounds.targets to their test project
  3. Reference app with <AdditionalProperties>Configuration=Library$(Configuration)</AdditionalProperties>
  4. Call AddResourceDictionary<Colors>() and AddResourceDictionary<Styles>() in test app setup
  5. Write tests that create real pages/controls with full style resolution

Once upstream fixes ship

  • Remove _RemoveImportedAppResizetizerItems from test-side targets (after maui#35575)
  • Remove _FixWindowsPriForAppReference from test-side targets (after WindowsAppSDK fix)
  • App-side targets remain permanently (they ARE the library-mode mechanism)

mattleibow and others added 15 commits May 22, 2026 21:29
- Add DeviceTestingKitApp.AppTests project that references the real app
  as a library (IsTestLibrary=true strips entry points and resources)
- Add AddResourceDictionary<T>() convenience method on the config builder
  for cleaner usage in lambda expressions
- Add x:Class to sample app's Colors.xaml and Styles.xaml to make them
  instantiable from test projects
- Add NUnit tests verifying MainPage instantiation and style resolution
- Wire up library-mode MSBuild in DeviceTestingKitApp.csproj (conditional
  OutputType, strip MauiIcon/Splash/Images/Fonts/Assets when IsTestLibrary)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Move service/VM/page/font registration into a shared extension method
  (ServiceCollectionExtensions.AddDeviceTestingKitAppServices) so both the
  real app and test projects call the same code
- Remove font files from AppTests (they flow from the app library reference)
- Simplify AppTests MauiProgram to just call the shared method
- Reduce IsTestLibrary conditions to only MauiIcon and MauiSplashScreen
  (images, fonts, and raw assets don't conflict)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Merge each resource dictionary into Application.Resources immediately
  after creation (not batch-then-merge). This ensures Styles.xaml can
  resolve {StaticResource Primary} from Colors.xaml during its
  InitializeComponent().
- Replace Application.Current.Resources lookups in RunStatusToColorConverter
  with hardcoded colors. Resources are now page-level scoped so the
  converter cannot reach them via Application.Resources, and during app
  teardown the lookup would throw KeyNotFoundException.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Verify CounterButton exists in MainPage visual tree
- Test initial button text renders correctly via converter
- Test IncrementCommand is wired to button and updates VM state
- Test multiple command executions via button.Command binding
- Add helper FindByAutomationId<T> for visual tree traversal

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Push MainPage as a modal onto the runner's navigation stack, then
execute the counter button's command and assert that the Button.Text
binding updates via the converter (Click me! → Clicked 1 time → ...).
Runs on the UI thread via MainThread.InvokeOnMainThreadAsync to satisfy
UIKit thread affinity requirements.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Explains the full pattern: making the app buildable as a library,
adding x:Class to resource dictionaries, creating the test project,
configuring AddResourceDictionary, and writing live binding tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resizetizer's transitive GetMauiItems target doesn't pass AdditionalProperties
from ProjectReference, so the app's MauiIcon leaks into the test project even
when IsTestLibrary=true strips it from the app's own build. Add a target that
removes the imported appicon after ResizetizeCollectItems runs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…unds.targets

Adopt Pedro's pattern: Configuration=Library$(Configuration) on ProjectReference
triggers IsTestProject=true in the app. This naturally gives separate intermediate
paths and eliminates file-locking race conditions during parallel solution builds.

Extract all MSBuild workaround logic into TestingWorkarounds.targets files in each
project folder to keep csproj files clean. Includes per-platform entry point
stripping, Resizetizer item removal, and Windows PRI fix.

Update documentation to reflect the new pattern.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Normalize backslashes to forward slashes before Contains() check so
the workaround works on both macOS and Windows CI agents.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add section explaining what to remove from TestingWorkarounds.targets
once the Resizetizer fix lands, and what must remain (Windows PRI
workaround and the app-side library mode targets).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Include complete TestingWorkarounds.targets as code fences that users
can copy directly. Explain why Configuration=Library$(Configuration) is
needed. Document the Windows PRI and Resizetizer issues with links to
dotnet/maui#35574 and #35575. Add section on what to remove once fix ships.

Tracking: #136

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When the app builds in library mode (Configuration=LibraryRelease), its
ProjectReferences to MauiLibrary/Library were still building as plain
Release. This caused a file-locking race with the solution-level build
of MauiLibrary (same config, same intermediate path, two MSBuild nodes
racing on XamlCTask).

Fix: propagate Configuration=LibraryRelease to transitive references
via AdditionalProperties when IsTestProject=true, giving them separate
output paths that cannot conflict with the solution-level build.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Propagating Configuration=Library$(Configuration) to transitive
references (MauiLibrary) caused APPX1101 duplicate payload errors on
Windows: the test app sees MauiLibrary.dll from both debug/ and
librarydebug/ output paths.

The original XamlCTask parallel race was a one-off flaky failure
(passed on ADO, failed once on GH Actions). Removing the propagation
fixes the Windows packaging error.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mattleibow mattleibow force-pushed the mattleibow/visual-runner-as-service branch from 6500747 to db9c4a5 Compare May 22, 2026 19:32
mattleibow and others added 4 commits May 22, 2026 21:47
Verify that styles from MauiLibrary's CounterStyles.xaml flow correctly
into the test app via AddResourceDictionary. Tests confirm:
- CounterButtonColor (#FF6B6B) is resolvable from Application.Resources
- CounterButtonTextColor (white) is resolvable
- Live button actually renders with the coral background, not default purple

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove internal MSBuild details (why Configuration=Library, Resizetizer/PRI
internals) and restructure as a quick-start guide for developers who want
to test their app's controls and services on device with real styles.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add DeviceRunners.Testing.Targets, VisualRunners.NUnit, and
  VisualRunners.Maui as PackageReference (not ProjectReference)
- Correct resource scoping description: implicit styles still apply
  globally by design

Found by Opus 4.7 and GPT 5.5 review.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…m order

- Change prereq from .NET 9+ to .NET 10+ (all examples use net10.0 TFMs)
- Add GenerateTestingPlatformEntryPoint=false to csproj snippet
- Fix FindByAutomationId pattern match: ContentPage arm before IContentView
- Add note that helper only covers simple ContentPage/Layout hierarchies

Found by Opus 4.7 and GPT 5.5 review.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant