Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d72a52c
docs(mocking): restore MVUX mocking & previews spec after workspace loss
dr1rrb Aug 24, 2026
49c37d3
docs(mocking): visual pass — mermaid diagrams for review support
dr1rrb Aug 24, 2026
cc4a510
docs: update the MVUX mocking spec
dr1rrb Aug 24, 2026
51c791c
docs(mocking): move the mocking gate to the source context and make t…
dr1rrb Aug 25, 2026
57c8d73
feat(mocking): gate the hot-swap wrap on the source context
dr1rrb Aug 25, 2026
a5ed1d0
feat(mocking): emit the feed dependency metadata from the MVUX generator
dr1rrb Aug 25, 2026
007e88c
feat(mocking): add the view-model command seam
dr1rrb Aug 25, 2026
5852d9b
feat(mocking): add the mocking runtime
dr1rrb Aug 25, 2026
f4f5aff
feat(mocking): add the consumer generator and make the swap fail hard
dr1rrb Aug 25, 2026
6510bbc
feat(mocking): complete the pinned feed vocabulary
dr1rrb Aug 25, 2026
90aea90
feat(mocking): add Create(), Empty, command wiring and the command vo…
dr1rrb Aug 25, 2026
7cd1650
docs(mocking): record the tier 2 and 3 implementation status
dr1rrb Aug 25, 2026
fb88e5a
refactor(mocking): reuse the FeedMock vocabulary shipped by #3149
dr1rrb Aug 25, 2026
fa1d928
fix(mocking): keep the ambient AsyncLocal in MockingService, not in S…
dr1rrb Aug 25, 2026
73b05a5
docs(mocking): document the generated mocks and add a catalog sample
dr1rrb Aug 25, 2026
ba05821
refactor(mocking): rename SetModel to SetMock and move the scope into…
dr1rrb Aug 28, 2026
29714cd
refactor(mocking): address the relevant code-quality review comments
dr1rrb Sep 2, 2026
149b987
fix(ci): add the missing XML docs and fix markdownlint on the specs
dr1rrb Sep 2, 2026
4a3e1b3
docs(mocking): translate the history document to English
dr1rrb Sep 2, 2026
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
75 changes: 75 additions & 0 deletions Uno.Extensions.sln
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,18 @@ EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Uno.Extensions.Authentication.Tests", "src\Uno.Extensions.Authentication.Tests\Uno.Extensions.Authentication.Tests.csproj", "{7AD671A6-E401-4C16-803B-C9554673FD19}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Uno.Extensions.Storage.WinUI.Tests", "src\Uno.Extensions.Storage.UI.Tests\Uno.Extensions.Storage.WinUI.Tests.csproj", "{26DA9186-8DD8-4623-A455-0F0B9DA2E79A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Uno.HotTesting.Reactive", "src\Uno.HotTesting.Reactive\Uno.HotTesting.Reactive.csproj", "{A1A08FE3-19D4-4EB2-B228-10187E2C4CDF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Uno.HotTesting.Reactive.Tests", "src\Uno.HotTesting.Reactive.Tests\Uno.HotTesting.Reactive.Tests.csproj", "{2347B2F1-002C-4165-B523-D4D236A13CEC}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Uno.Extensions.Reactive.Mocking", "src\Uno.Extensions.Reactive.Mocking\Uno.Extensions.Reactive.Mocking.csproj", "{C1D1F711-4271-4E79-AABD-AA316F11D8C3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Uno.Extensions.Authentication.MSAL.Tests", "src\Uno.Extensions.Authentication.MSAL.Tests\Uno.Extensions.Authentication.MSAL.Tests.csproj", "{E5EA0457-2031-48A7-9C28-90B94A3861DF}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Uno.Extensions.Reactive.Mocking.Generator", "src\Uno.Extensions.Reactive.Mocking.Generator\Uno.Extensions.Reactive.Mocking.Generator.csproj", "{62D73733-0DB2-407F-92A8-01FA1EA675EB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Uno.Extensions.Reactive.Tests.MockingApp", "src\Uno.Extensions.Reactive.Tests.MockingApp\Uno.Extensions.Reactive.Tests.MockingApp.csproj", "{C16D5143-8353-400D-BE40-9CEEEB2A5404}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Uno.HotTesting.Reactive.Generator", "src\Uno.HotTesting.Reactive.Generator\Uno.HotTesting.Reactive.Generator.csproj", "{94FC36D9-81B5-4347-8EBE-1DF99DB55981}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -914,6 +921,70 @@ Global
{E5EA0457-2031-48A7-9C28-90B94A3861DF}.Release|x64.Build.0 = Release|Any CPU
{E5EA0457-2031-48A7-9C28-90B94A3861DF}.Release|x86.ActiveCfg = Release|Any CPU
{E5EA0457-2031-48A7-9C28-90B94A3861DF}.Release|x86.Build.0 = Release|Any CPU
{C1D1F711-4271-4E79-AABD-AA316F11D8C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C1D1F711-4271-4E79-AABD-AA316F11D8C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C1D1F711-4271-4E79-AABD-AA316F11D8C3}.Debug|arm64.ActiveCfg = Debug|Any CPU
{C1D1F711-4271-4E79-AABD-AA316F11D8C3}.Debug|arm64.Build.0 = Debug|Any CPU
{C1D1F711-4271-4E79-AABD-AA316F11D8C3}.Debug|x64.ActiveCfg = Debug|Any CPU
{C1D1F711-4271-4E79-AABD-AA316F11D8C3}.Debug|x64.Build.0 = Debug|Any CPU
{C1D1F711-4271-4E79-AABD-AA316F11D8C3}.Debug|x86.ActiveCfg = Debug|Any CPU
{C1D1F711-4271-4E79-AABD-AA316F11D8C3}.Debug|x86.Build.0 = Debug|Any CPU
{C1D1F711-4271-4E79-AABD-AA316F11D8C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C1D1F711-4271-4E79-AABD-AA316F11D8C3}.Release|Any CPU.Build.0 = Release|Any CPU
{C1D1F711-4271-4E79-AABD-AA316F11D8C3}.Release|arm64.ActiveCfg = Release|Any CPU
{C1D1F711-4271-4E79-AABD-AA316F11D8C3}.Release|arm64.Build.0 = Release|Any CPU
{C1D1F711-4271-4E79-AABD-AA316F11D8C3}.Release|x64.ActiveCfg = Release|Any CPU
{C1D1F711-4271-4E79-AABD-AA316F11D8C3}.Release|x64.Build.0 = Release|Any CPU
{C1D1F711-4271-4E79-AABD-AA316F11D8C3}.Release|x86.ActiveCfg = Release|Any CPU
{C1D1F711-4271-4E79-AABD-AA316F11D8C3}.Release|x86.Build.0 = Release|Any CPU
{62D73733-0DB2-407F-92A8-01FA1EA675EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{62D73733-0DB2-407F-92A8-01FA1EA675EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{62D73733-0DB2-407F-92A8-01FA1EA675EB}.Debug|arm64.ActiveCfg = Debug|Any CPU
{62D73733-0DB2-407F-92A8-01FA1EA675EB}.Debug|arm64.Build.0 = Debug|Any CPU
{62D73733-0DB2-407F-92A8-01FA1EA675EB}.Debug|x64.ActiveCfg = Debug|Any CPU
{62D73733-0DB2-407F-92A8-01FA1EA675EB}.Debug|x64.Build.0 = Debug|Any CPU
{62D73733-0DB2-407F-92A8-01FA1EA675EB}.Debug|x86.ActiveCfg = Debug|Any CPU
{62D73733-0DB2-407F-92A8-01FA1EA675EB}.Debug|x86.Build.0 = Debug|Any CPU
{62D73733-0DB2-407F-92A8-01FA1EA675EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{62D73733-0DB2-407F-92A8-01FA1EA675EB}.Release|Any CPU.Build.0 = Release|Any CPU
{62D73733-0DB2-407F-92A8-01FA1EA675EB}.Release|arm64.ActiveCfg = Release|Any CPU
{62D73733-0DB2-407F-92A8-01FA1EA675EB}.Release|arm64.Build.0 = Release|Any CPU
{62D73733-0DB2-407F-92A8-01FA1EA675EB}.Release|x64.ActiveCfg = Release|Any CPU
{62D73733-0DB2-407F-92A8-01FA1EA675EB}.Release|x64.Build.0 = Release|Any CPU
{62D73733-0DB2-407F-92A8-01FA1EA675EB}.Release|x86.ActiveCfg = Release|Any CPU
{62D73733-0DB2-407F-92A8-01FA1EA675EB}.Release|x86.Build.0 = Release|Any CPU
{C16D5143-8353-400D-BE40-9CEEEB2A5404}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C16D5143-8353-400D-BE40-9CEEEB2A5404}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C16D5143-8353-400D-BE40-9CEEEB2A5404}.Debug|arm64.ActiveCfg = Debug|Any CPU
{C16D5143-8353-400D-BE40-9CEEEB2A5404}.Debug|arm64.Build.0 = Debug|Any CPU
{C16D5143-8353-400D-BE40-9CEEEB2A5404}.Debug|x64.ActiveCfg = Debug|Any CPU
{C16D5143-8353-400D-BE40-9CEEEB2A5404}.Debug|x64.Build.0 = Debug|Any CPU
{C16D5143-8353-400D-BE40-9CEEEB2A5404}.Debug|x86.ActiveCfg = Debug|Any CPU
{C16D5143-8353-400D-BE40-9CEEEB2A5404}.Debug|x86.Build.0 = Debug|Any CPU
{C16D5143-8353-400D-BE40-9CEEEB2A5404}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C16D5143-8353-400D-BE40-9CEEEB2A5404}.Release|Any CPU.Build.0 = Release|Any CPU
{C16D5143-8353-400D-BE40-9CEEEB2A5404}.Release|arm64.ActiveCfg = Release|Any CPU
{C16D5143-8353-400D-BE40-9CEEEB2A5404}.Release|arm64.Build.0 = Release|Any CPU
{C16D5143-8353-400D-BE40-9CEEEB2A5404}.Release|x64.ActiveCfg = Release|Any CPU
{C16D5143-8353-400D-BE40-9CEEEB2A5404}.Release|x64.Build.0 = Release|Any CPU
{C16D5143-8353-400D-BE40-9CEEEB2A5404}.Release|x86.ActiveCfg = Release|Any CPU
{C16D5143-8353-400D-BE40-9CEEEB2A5404}.Release|x86.Build.0 = Release|Any CPU
{94FC36D9-81B5-4347-8EBE-1DF99DB55981}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{94FC36D9-81B5-4347-8EBE-1DF99DB55981}.Debug|Any CPU.Build.0 = Debug|Any CPU
{94FC36D9-81B5-4347-8EBE-1DF99DB55981}.Debug|arm64.ActiveCfg = Debug|Any CPU
{94FC36D9-81B5-4347-8EBE-1DF99DB55981}.Debug|arm64.Build.0 = Debug|Any CPU
{94FC36D9-81B5-4347-8EBE-1DF99DB55981}.Debug|x64.ActiveCfg = Debug|Any CPU
{94FC36D9-81B5-4347-8EBE-1DF99DB55981}.Debug|x64.Build.0 = Debug|Any CPU
{94FC36D9-81B5-4347-8EBE-1DF99DB55981}.Debug|x86.ActiveCfg = Debug|Any CPU
{94FC36D9-81B5-4347-8EBE-1DF99DB55981}.Debug|x86.Build.0 = Debug|Any CPU
{94FC36D9-81B5-4347-8EBE-1DF99DB55981}.Release|Any CPU.ActiveCfg = Release|Any CPU
{94FC36D9-81B5-4347-8EBE-1DF99DB55981}.Release|Any CPU.Build.0 = Release|Any CPU
{94FC36D9-81B5-4347-8EBE-1DF99DB55981}.Release|arm64.ActiveCfg = Release|Any CPU
{94FC36D9-81B5-4347-8EBE-1DF99DB55981}.Release|arm64.Build.0 = Release|Any CPU
{94FC36D9-81B5-4347-8EBE-1DF99DB55981}.Release|x64.ActiveCfg = Release|Any CPU
{94FC36D9-81B5-4347-8EBE-1DF99DB55981}.Release|x64.Build.0 = Release|Any CPU
{94FC36D9-81B5-4347-8EBE-1DF99DB55981}.Release|x86.ActiveCfg = Release|Any CPU
{94FC36D9-81B5-4347-8EBE-1DF99DB55981}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -970,6 +1041,10 @@ Global
{A1A08FE3-19D4-4EB2-B228-10187E2C4CDF} = {6B956AB1-06A6-4BEC-9467-E7B593A89E34}
{2347B2F1-002C-4165-B523-D4D236A13CEC} = {6B956AB1-06A6-4BEC-9467-E7B593A89E34}
{E5EA0457-2031-48A7-9C28-90B94A3861DF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{C1D1F711-4271-4E79-AABD-AA316F11D8C3} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{62D73733-0DB2-407F-92A8-01FA1EA675EB} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{C16D5143-8353-400D-BE40-9CEEEB2A5404} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{94FC36D9-81B5-4347-8EBE-1DF99DB55981} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6E7B035D-9A64-4D95-89AA-9D4653F17C42}
Expand Down
118 changes: 115 additions & 3 deletions doc/Reference/Reactive/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,118 @@ public sealed partial class MySuperPage : Page
```

The developer remains responsible for the property names, feed shapes, and
`DataContext` injection. These factories are the reusable runtime base for the
longer-term Hot Testing direction; this initial API does not promise or require a
generator.
`DataContext` injection. This handwritten approach needs no generator and is handy
for pages without a view-model; for a real generated view-model driven through
mocked states, use the generated mocks described below.

## Generated view-model mocks (Hot Testing)

The handwritten record above is enough for a page that has no view-model, but as
soon as you want to drive a **real generated view-model** — its real `Model`, its
real business logic — through mocked states, let the `Uno.HotTesting.Reactive`
generator build the plumbing for you.

Reference the `Uno.HotTesting.Reactive` package in the **test or preview project**
(the one that references the app). For every MVUX model it finds, the generator
emits, next to the model:

- a `record {Model}Mock` whose **required** members are exactly the
service-dependent feeds, and whose **optional** members are the derived feeds
(a `{Model}Mock.Empty` pins every input to its empty state);
- a `partial class {Vm}Mock` with `Create(...)` factories that build the **real
view-model** with its services null-injected, then apply the mock;
- a `SetMock(this {Vm}, {Model}Mock)` extension that swaps each mocked feed.

Given this model:

```csharp
public partial record RecipeModel(IRecipeService Service)
{
// service-dependent input
public IListFeed<Step> Steps => ListFeed.Async(Service.GetSteps);

// derived — recomputes over whatever Steps emits
public IFeed<int> StepsCount => Steps.Select(steps => steps.Count);
}
```

the generator produces `RecipeModelMock`, `RecipeViewModelMock.Create(...)` and
`SetMock`. You drive the page from a test or a preview head — no scope to open,
`Create` opens it internally:

```csharp
using Uno.HotTesting.Reactive;

// Real RecipeViewModel + real RecipeModel, IRecipeService null-injected.
var vm = RecipeViewModelMock.Create(new RecipeModelMock { Steps = ListFeedMock.Value(step1, step2, step3) });

// StepsCount is NOT mocked — it recomputes through the real Select over the
// mocked Steps, so the derived feed stays truthful.

// Live transitions: keep calling SetMock to walk the states.
vm.SetMock(RecipeModelMock.Empty with { Steps = ListFeedMock.Loading<Step>() });
vm.SetMock(RecipeModelMock.Empty with { Steps = ListFeedMock.Error<Step>(new TimeoutException()) });
```

> [!IMPORTANT]
> Mocking is only active for a view-model built by `Create` (which opens a
> `MockingService.Enable()` scope around construction). Outside such a scope
> nothing is wrapped, so a shipping application pays no cost — never reference
> `Uno.HotTesting.Reactive` from a published app head.

### Only fill what matters

Required members are the **service-dependent inputs**; the compiler lists them
for you. Derived members are optional:

```csharp
// every input at once, exhaustively
vm.SetMock(new RecipeModelMock
{
Steps = ListFeedMock.Loading<Step>(), // required — the compiler asked for it
// StepsCount left unset → real derivation runs over the mocked Steps
});

// or start from Empty and change one axis with `with`
vm.SetMock(RecipeModelMock.Empty with { Steps = ListFeedMock.Loading<Step>() });

// or pin a derived value directly, ignoring its inputs
vm.SetMock(RecipeModelMock.Empty with { StepsCount = FeedMock.Value(3) });
```

> [!NOTE]
> Mocking commands is not supported yet — it is planned for a future version.

### Named catalogs (one-line previews)

`{Vm}Mock` is generated as a **partial class**, so you extend it with named
catalog entries in your own file. Each entry builds the real view-model pinned to
a state through `Create`:

```csharp
// your file, same namespace as the generated RecipeViewModelMock
public static partial class RecipeViewModelMock
{
public static RecipeViewModel Loading => Create(RecipeModelMock.Empty with { Steps = ListFeedMock.Loading<Step>() });
public static RecipeViewModel Empty => Create(); // every input Empty
public static RecipeViewModel Basic => Create(new RecipeModelMock
{
Steps = ListFeedMock.Value(new Step("Toast the bread"), new Step("Mash the avocado")),
});
}
```

```xml
<!-- One-line preview binding — real page, real view-model, pinned state -->
<Page DataContext="{x:Bind mocks:RecipeViewModelMock.Basic}" />
```

### Turning the instrumentation off

The metadata the consumer generator reads is emitted **by default** (the runtime,
not the generator, decides activation). To restore byte-identical MVUX output,
opt out at the assembly level:

```csharp
[assembly: EnableFeedMocking(IsEnabled = false)]
```
Loading
Loading