Runic.Application.Testing
0.7.0-preview.1
See the version list below for details.
dotnet add package Runic.Application.Testing --version 0.7.0-preview.1
NuGet\Install-Package Runic.Application.Testing -Version 0.7.0-preview.1
<PackageReference Include="Runic.Application.Testing" Version="0.7.0-preview.1" />
<PackageVersion Include="Runic.Application.Testing" Version="0.7.0-preview.1" />
<PackageReference Include="Runic.Application.Testing" />
paket add Runic.Application.Testing --version 0.7.0-preview.1
#r "nuget: Runic.Application.Testing, 0.7.0-preview.1"
#:package Runic.Application.Testing@0.7.0-preview.1
#addin nuget:?package=Runic.Application.Testing&version=0.7.0-preview.1&prerelease
#tool nuget:?package=Runic.Application.Testing&version=0.7.0-preview.1&prerelease
Runic.Application.Testing
Test a generated Runic Window contract and its routed Views without a browser or
native window. RunicWindowTestHost<TViewModel> owns an in-memory transport, a real
WindowContentSession, and the generated root Bridge attachment, so tests run the
real ViewModels and generated .NET handlers. The caller owns the ViewModel and its
dependency injection scope.
Testing
Resolve the window's ViewModel and the generated attachment from the application's own services, with a test clock in place of the system clock:
var clock = new FakeTimeProvider(); // Microsoft.Extensions.Time.Testing
using var services = new ServiceCollection()
.AddSingleton<TimeProvider>(clock)
.AddNotes() // the application's registrations
.AddRunicViews() // generated by RunicBridgeCompositionType
.BuildServiceProvider();
using var scope = services.CreateScope();
using var host = new RunicWindowTestHost<ShellViewModel>(
scope.ServiceProvider.GetRequiredService<ShellViewModel>(),
scope.ServiceProvider.GetRequiredService<Func<IBridgeTransport, WindowContentSession, ShellViewModel, IDisposable>>(),
new RunicWindowTestHostOptions
{
ViewLocator = scope.ServiceProvider.GetRequiredService<IRunicViewLocator>(),
TimeProvider = clock,
});
Then drive the Window by ViewModel member, as the generated TypeScript client does:
var sidebar = host.Root.View<SidebarViewModel>(vm => vm.Sidebar);
(await sidebar.ExecuteAsync(vm => vm.OpenNotesCommand)).EnsureOk();
var editor = host.Root.View<DocumentViewModel>(vm => vm.Main)
.View<EditorViewModel>(vm => vm.CurrentPane);
editor.Set(vm => vm.Title, "Groceries").EnsureOk();
var save = editor.Start(vm => vm.SaveCommand);
Assert.True(editor.Snapshot().IsExecuting(vm => vm.SaveCommand));
clock.Advance(TimeSpan.FromMilliseconds(250)); // storage waits on the injected clock
Assert.Equal("succeeded", (await save.WaitAsync()).Kind);
Assert.Equal("Saved Groceries", editor.Snapshot().Read(vm => vm.SavedMessage));
The CommunityToolkit Notes example is a complete xUnit project.
Deterministic time and ids
With RunicWindowTestHostOptions, the window uses a
FakeTimeProvider
from Microsoft.Extensions.TimeProvider.Testing, which this package depends on, unless
the options supply another TimeProvider. Its time passes only through Advance or
SetUtcNow, which fire due timers, CancellationTokenSource timeouts and
Task.Delay(TimeSpan, TimeProvider) in order; a timer due now fires at once. The
window uses it for interaction deadlines and BeginCloseAsync timeouts; register
the same instance in the application's services for its own time. host.Time
returns it.
Work that waits on the clock does not finish by itself:
ExecuteAsyncof an awaited command that waits on the clock completes only after the test advances it. Start the call, advance the clock, then await the call:var save = editor.ExecuteAsync(vm => vm.SaveCommand); clock.Advance(TimeSpan.FromMilliseconds(250)); (await save).EnsureOk();Startreturns at admission, so an operation can be inspected while it runs.Only code that takes the
TimeProviderfollows it.Task.Delay(TimeSpan),DateTime.UtcNowand ReactiveUI's schedulers (Throttle,Timer,RxApp.MainThreadScheduler) keep using real time. Give such code the clock, for example through a scheduler built on it, or test it with ReactiveUI'sTestScheduler.
Content ids are 1, 2, ... in presentation order, so routes are content1,
content2, ... and snapshots are stable. Set CreateId for another sequence. The
drivers issue mount tokens test:mount1, ... and operation request ids request1,
....
The constructor without options keeps the system clock and random ids.
Drivers
host.Root drives the root ViewModel. View<T>(vm => vm.Content) and
Views<T>(vm => vm.Items) follow the content references in the current snapshot
and check that they present T; host.View<T>(reference) drives a reference you
already have. A RunicViewDriver<T> has:
Snapshot(): aRunicViewState<T>withRead(vm => vm.Title)for scalars,Getfor the raw JSON,Reference,References,Keysof a[RunicCollection],CanExecute,IsExecutingandFieldVersion.Set(vm => vm.Title, value), encoded as the generated client encodes it: anint,boolor non-nullablestringas is, and other scalars as wire JSON (Int64anddecimalas strings, enums by wire name,nullfor nullable values). UseSetJsonwith the wire JSON of a DTO, list or union.ExecuteAsync(vm => vm.SaveCommand, argumentJson), which waits for an asynchronous command, andCanExecute(command, argumentJson)for commands with an argument. A reply hasOk,ErrorKind,ErrorMessageandState;EnsureOk()throws for a failed call. A command's declared failure ([RunicFailure]) replies withErrorKinddomain-failedand the encoded failure asFailure, such as{"$case":"titleRequired"}.Start(command, argumentJson, requestId)for a recoverable operation, withStatus(),WaitAsync()andCancel(). A status hasKind,Result,ErrorMessageand, for adomain-failedoperation,Failure.Write(vm => vm.Title, value, expectedVersion)for a checked write. It uses the current value and version as the baseline unlessexpectedVersionis given, so a stale version tests theconflictreceipt.Mount()andUnmount(token)acknowledge a browser presentation.TakePublications()andNextPublicationAsync()return this route's pushed frames in order:State,Deltawith itsRunicCollectionChanges, orFailure.
Collections and deltas
Track() follows a route from a snapshot through its frames, as the generated
client does: newer full states replace the state, deltas must continue the tracked
revision and name the row keys they change, and failure notices are collected.
A frame the client could not apply throws, naming the problem: a lost or reordered
frame, an index outside the rows, a row key that does not match, an item count that
does not match the keys, or an empty or duplicate key.
var tracker = host.Root.View<HomeViewModel>(vm => vm.Main).Track();
library.Record("Groceries", "Milk, eggs");
var state = await tracker.WaitUntilAsync(state => state.Keys(vm => vm.RecentNotes)[0] == "Groceries");
Assert.Equal(0, tracker.FullStates); // only keyed changes were sent
Assert.Contains(tracker.Changes, change => change.Kind == "move");
tracker.Verify(); // the frames match a fresh snapshot
Publications are delivered off the calling thread, so waits use real time,
RunicWindowTestHostOptions.PublicationTimeout (five seconds by default). A route's
frames go to its tracker or to TakePublications, not both.
Lower level
host.Transport is the in-memory transport: Call(...) and CallAsync(...) invoke
any route with ViewTestArguments, and DrainPublications() returns raw
publications (drivers then miss them). Mount, Unmount and
Content.ReleaseConnection test View lifetimes and client disconnects. Await
BeginCloseAsync before disposing a host that has accepted background operations;
with a fake clock, its timeout passes only when the clock advances.
The host does not instantiate the application's native Window wrapper or run the
generated TypeScript client or a rendering framework. Test the frontend with the
generated typed mocks of @runic-artifex/views/mock; see its
Testing guide.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.12)
- Microsoft.Extensions.TimeProvider.Testing (>= 10.10.0)
- Runic.Application (= 0.7.0-preview.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.8.0-preview.2 | 0 | 10/15/2026 |
| 0.8.0-preview.1 | 0 | 10/15/2026 |
| 0.7.0-preview.9 | 363 | 10/14/2026 |
| 0.7.0-preview.8 | 76 | 10/12/2026 |
| 0.7.0-preview.7 | 50 | 10/12/2026 |
| 0.7.0-preview.6 | 394 | 10/9/2026 |
| 0.7.0-preview.5 | 126 | 10/9/2026 |
| 0.7.0-preview.4 | 71 | 10/8/2026 |
| 0.7.0-preview.3 | 61 | 10/8/2026 |
| 0.7.0-preview.2 | 64 | 10/7/2026 |
| 0.7.0-preview.1 | 58 | 10/7/2026 |
| 0.6.0-preview.1 | 557 | 10/5/2026 |
| 0.5.0-preview.3 | 68 | 10/1/2026 |
| 0.5.0-preview.2 | 65 | 9/25/2026 |
| 0.4.0-preview.1 | 66 | 9/22/2026 |
| 0.3.0-preview.1 | 79 | 9/11/2026 |
| 0.2.0-preview.1 | 93 | 9/9/2026 |