|
| 1 | +--- |
| 2 | +uid: emulating-ijsruntime |
| 3 | +title: Emulating Blazor's IJSRuntime |
| 4 | +--- |
| 5 | + |
| 6 | +# Emulating Blazor's `IJSRuntime` |
| 7 | + |
| 8 | +It is common for Blazor components to use `IJSRuntime` to call JavaScript, and since bUnit does not run JavaScript, emulating `IJSRuntime` is needed for components that uses it. In that regard, `IJSRuntime` is no different than other services that a component might depend on. |
| 9 | + |
| 10 | +bUnit comes with it's own JSInterop, a tailor built implementation of `IJSRuntime` that is _active by default_, allowing you to specify how JavaScript interop calls should be handled, what values they calls should return, and also allowing you to verify that they the calls have happened. The implementation is running in "strict mode", which means means it will throw an exception if it receives an invocation it has not been configured to handle. See more about strict vs loose mode in the following section. |
| 11 | + |
| 12 | +If you prefer to use the same mocking framework for all mocking in your tests to keep things consistent, general purpose mocking frameworks like [Moq](https://github.com/Moq), [JustMock Lite](https://github.com/telerik/JustMockLite), or [NSubstitute](https://nsubstitute.github.io/) all works nicely with bUnit and can be used to mock `IJSRuntime`. In general, registering an implementation of `IJSRuntime` with bUnit's `Services` collection replaces bUnit's implementation. |
| 13 | + |
| 14 | +The following sections shows how to use the built-in implementation of `IJSRuntime`. |
| 15 | + |
| 16 | +> [!NOTE] |
| 17 | +> In the beta versions of bUnit, you had to explicitly add the mock JSRuntime by calling `Services.AddMockJSRuntime()`. That is no longer needed, and doesn't work any more. |
| 18 | +
|
| 19 | +## Strict vs loose mode |
| 20 | + |
| 21 | +bUnit's JSInterop can run in two modes, **strict** or **loose**: |
| 22 | + |
| 23 | +- **Loose** mode configures the implementation to just return the default value when it receives an invocation that has not been explicitly set up, e.g. if a component calls `InvokeAsync<int>(...)` the mock will simply return `default(int)` back to it immediately. |
| 24 | +- **Strict** mode configures the implementation to throw an exception if it is invoked with a method call it has _not_ been set up to handle explicitly. This is useful if you want to ensure that a component only performs a specific set of `IJSRuntime` invocations. |
| 25 | + |
| 26 | +By default, the bUnit's JSInterop runs in **Strict** mode. To change the mode, do the following: |
| 27 | + |
| 28 | +```csharp |
| 29 | +using var ctx = new TestContext(); |
| 30 | +ctx.JSInterop.Mode = JSRuntimeMockMode.Loose; |
| 31 | +``` |
| 32 | + |
| 33 | +## Setting up invocations |
| 34 | + |
| 35 | +Use the `Setup<TResult>(...)` and `SetupVoid(...)` methods to configure the implementation to handle calls from the **matching** `InvokeAsync<TResult>(...)` and `InvokeVoidAsync(...)` methods on `IJSRuntime`. |
| 36 | + |
| 37 | +Use the parameterless `Setup<TResult>()` method to emulate any call to `InvokeAsync<TResult>(...)` with a given return type `TResult` and use the parameterless `SetupVoid()` to emulate any call to `InvokeVoidAsync(...)`. |
| 38 | + |
| 39 | +When an invocation is set up through of the `Setup<TResult>(...)` and `SetupVoid(...)` methods, a `JSRuntimePlannedInvocation<TResult>` object is returned. This can be used to set a result or an exception, to emulate what can happen during a JavaScript interop call in Blazor. |
| 40 | + |
| 41 | +Similarly when the parameterless `Setup<TResult>()` and `SetupVoid()` methods are used a `JSRuntimeCatchAllPlannedInvocation<TResult>` object is returned which can be used to set the result of invocation. |
| 42 | + |
| 43 | +Here are two examples: |
| 44 | + |
| 45 | +```csharp |
| 46 | +using var ctx = new TestContext(); |
| 47 | + |
| 48 | +// Set up an invocation and specify the result value immediately |
| 49 | +ctx.JSInterop.Setup<string>("getPageTitle").SetResult("bUnit is awesome"); |
| 50 | + |
| 51 | +// Set up an invocation without specifying the result |
| 52 | +var plannedInvocation = ctx.JSInterop.SetupVoid("startAnimation"); |
| 53 | + |
| 54 | +// ... other test code |
| 55 | +
|
| 56 | +// Later in the test, mark the invocation as completed. |
| 57 | +// SetResult() is not used in this case since InvokeVoidAsync |
| 58 | +// only completes or throws, it doesn’t return a value. |
| 59 | +// Any calls to InvokeVoidAsync(...) up till this point will |
| 60 | +// have received an incompleted Task which the component |
| 61 | +// is likely waiting until the call to SetCompleted() below. |
| 62 | +plannedInvocation.SetCompleted(); |
| 63 | +``` |
| 64 | + |
| 65 | +## Verifying invocations |
| 66 | + |
| 67 | +All calls to the `InvokeAsync<TResult>(...)` and `InvokeVoidAsync(...)` methods in bUnit's JSInterop are stored in its `Invocations` list, which can be inspected and asserted against. In addition to this, all planned invocations have their own `Invocations` list which only contain their invocations. |
| 68 | + |
| 69 | +Invocations are represented by the `JSRuntimeInvocation` type which has three properties of interest when verifying an invocation happened as expected: |
| 70 | + |
| 71 | +- `Identifier` - the name of the function name/identifier passed to the invoke method. |
| 72 | +- `Arguments` - a list of arguments passed to the invoke method. |
| 73 | +- `CancellationToken` - the cancellation token passed to the invoke method (if any). |
| 74 | + |
| 75 | +To verify these, just use the assertion methods you normally use. |
| 76 | + |
| 77 | +### Support for `IJSInProcessRuntime` and `IJSUnmarshalledRuntime` |
| 78 | + |
| 79 | +bUnit's `IJSRuntime` supports being cast to the `IJSInProcessRuntime` and `IJSUnmarshalledRuntime` types, just like Blazors `IJSRuntime`. |
| 80 | + |
| 81 | +To set up a handler for a `Invoke` and `InvokeUnmarshalled` call, just use the regular `Setup` and `SetupVoid` methods on bUnit's JSInterop. |
| 82 | + |
| 83 | +## Support for importing JavaScript Modules |
| 84 | + |
| 85 | +Since the .NET 5 release of Blazor, it has been possible to import JavaScript modules directly from components. This is supported by bUnit's JSInterop through the `SetupModule` methods, that setup calls to `InvokeAsync<IJSObjectReference>`. |
| 86 | + |
| 87 | +The `SetupModule` methods return a module JSInterop, that can be configured to handle the any JavaScript calls using the `Setup` and `SetupVoid` methods. For example, to configrue bUnit's JSInterop to handle an import of the JavaScript module `hello.js`, and a call to the function `world()` in that model, do the following: |
| 88 | + |
| 89 | +```csharp |
| 90 | +using var ctx = new TestContext(); |
| 91 | + |
| 92 | +var moduleInterop = ctx.JSInterop.SetupModule("hello.js"); |
| 93 | +moduleInterop.SetupVoid("world"); |
| 94 | +``` |
| 95 | + |
| 96 | +### Module Interop Mode |
| 97 | + |
| 98 | +By default, a module Interop inherits the `Mode` setting from the root JSInterop in bUnit. However, you can override it explictly and have it in a different mode from other module Interop or the root JSInterop. Just set the `Mode` property, e.g.: |
| 99 | + |
| 100 | +```csharp |
| 101 | +var moduleInterop = ctx.JSInterop.SetupModule("hello.js"); |
| 102 | +moduleInterop.Mode = JSRuntimeMockMode.Loose; |
| 103 | +``` |
| 104 | + |
| 105 | +### Support for `IJSInProcessObjectReference` and `IJSUnmarshalledObjectReference` |
| 106 | + |
| 107 | +bUnit's `IJSObjectReference` supports being cast to the `IJSInProcessObjectReference` and `IJSUnmarshalledObjectReference` types, just like Blazors `IJSObjectReference`. |
| 108 | + |
| 109 | +To set up a handler for a `Invoke` and `InvokeUnmarshalled` call, just use the regular `Setup` and `SetupVoid` methods on bUnit's JSInterop. |
| 110 | + |
| 111 | +## First Party JSInterop Component Emulation |
| 112 | + |
| 113 | +Blazor comes out of the box with a few components that requires a working JSInterop. bUnit's JSInterop is setup to emulate the JavaScript interactions of those components. The following sections describes how the interaction is emulated for the supported components. |
| 114 | + |
| 115 | +### <Virtualize> JSInterop Emulation |
| 116 | + |
| 117 | +The `<Virtualize>` component require JavaScript to notify it about the available screen space it is being rendered to, and when the users scrolls the viewport, to trigger the loading of new data. bUnit emulates this interaction by telling the `<Virtualize>` component that the viewport is `1,000,000,000` pixels large. That should ensure that all items is loaded, which makes sense in a testing scenario. |
| 118 | + |
| 119 | +To test the `<Placeholder>` template of the `<Virtualize>` component, create a items provider that doesn't return all items when queried. |
| 120 | + |
| 121 | +### FocusAsync JSInterop Emulation |
| 122 | + |
| 123 | +Support for the [`FocusAsync`](https://docs.microsoft.com/en-us/aspnet/core/blazor/components/event-handling?view=aspnetcore-5.0#focus-an-element) method on `ElementReference` in Blazor's .NET 5 release works by simply registering the invocations, which can then be verified to have happened. |
| 124 | + |
| 125 | +To verify that the `FocusAsync` has been called in the `<ClickToFocus>` component: |
| 126 | + |
| 127 | +```cshtml |
| 128 | +<input @ref="exampleInput" /> |
| 129 | +
|
| 130 | +<button @onclick="ChangeFocus">Focus the Input Element</button> |
| 131 | +
|
| 132 | +@code { |
| 133 | + private ElementReference exampleInput; |
| 134 | + private async Task ChangeFocus() |
| 135 | + { |
| 136 | + await exampleInput.FocusAsync(); |
| 137 | + } |
| 138 | +} |
| 139 | +``` |
| 140 | + |
| 141 | +Do the following: |
| 142 | + |
| 143 | +```csharp |
| 144 | +using var ctx = new TestContext(); |
| 145 | +var cut = RenderComponent<ClickToFocus>(); |
| 146 | +var inputElement = cut.Find("input"); |
| 147 | + |
| 148 | +cut.Find("button").Click(); // Triggers onclick handler that sets focus of input element |
| 149 | +
|
| 150 | +ctx.JSInterop.VerifyFocusAsyncInvoke() // Verifies that a FocusAsync call has happenend |
| 151 | + .Arguments[0] // gets the first argument passed to the FocusAsync method |
| 152 | + .ShouldBeElementReferenceTo(inputElement); // verify that it is an element reference to the input element. |
| 153 | +``` |
| 154 | + |
| 155 | +## Support for `IJSInProcessRuntime` and `IJSUnmarshalledRuntime` |
| 156 | + |
| 157 | +bUnit's `IJSRuntime` supports being cast to the `IJSInProcessRuntime` and `IJSUnmarshalledRuntime` types, just like Blazors `IJSRuntime`. |
| 158 | + |
| 159 | +To set up a handler for a `Invoke` and `InvokeUnmarshalled` call, just use the regular `Setup` and `SetupVoid` methods on bUnit's JSInterop. |
0 commit comments