Runic.Application.Testing 0.7.0-preview.1

This is a prerelease version of Runic.Application.Testing.
There is a newer prerelease version of this package available.
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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Runic.Application.Testing" Version="0.7.0-preview.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Runic.Application.Testing" Version="0.7.0-preview.1" />
                    
Directory.Packages.props
<PackageReference Include="Runic.Application.Testing" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Runic.Application.Testing --version 0.7.0-preview.1
                    
#r "nuget: Runic.Application.Testing, 0.7.0-preview.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Runic.Application.Testing@0.7.0-preview.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Runic.Application.Testing&version=0.7.0-preview.1&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Runic.Application.Testing&version=0.7.0-preview.1&prerelease
                    
Install as a Cake Tool

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:

  • ExecuteAsync of 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();
    

    Start returns at admission, so an operation can be inspected while it runs.

  • Only code that takes the TimeProvider follows it. Task.Delay(TimeSpan), DateTime.UtcNow and 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's TestScheduler.

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(): a RunicViewState<T> with Read(vm => vm.Title) for scalars, Get for the raw JSON, Reference, References, Keys of a [RunicCollection], CanExecute, IsExecuting and FieldVersion.
  • Set(vm => vm.Title, value), encoded as the generated client encodes it: an int, bool or non-nullable string as is, and other scalars as wire JSON (Int64 and decimal as strings, enums by wire name, null for nullable values). Use SetJson with the wire JSON of a DTO, list or union.
  • ExecuteAsync(vm => vm.SaveCommand, argumentJson), which waits for an asynchronous command, and CanExecute(command, argumentJson) for commands with an argument. A reply has Ok, ErrorKind, ErrorMessage and State; EnsureOk() throws for a failed call. A command's declared failure ([RunicFailure]) replies with ErrorKind domain-failed and the encoded failure as Failure, such as {"$case":"titleRequired"}.
  • Start(command, argumentJson, requestId) for a recoverable operation, with Status(), WaitAsync() and Cancel(). A status has Kind, Result, ErrorMessage and, for a domain-failed operation, Failure.
  • Write(vm => vm.Title, value, expectedVersion) for a checked write. It uses the current value and version as the baseline unless expectedVersion is given, so a stale version tests the conflict receipt.
  • Mount() and Unmount(token) acknowledge a browser presentation.
  • TakePublications() and NextPublicationAsync() return this route's pushed frames in order: State, Delta with its RunicCollectionChanges, or Failure.

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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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