Skip to content

Commit 51d2f7c

Browse files
committed
updates to docs and samples
1 parent bae0bd4 commit 51d2f7c

File tree

5 files changed

+351
-41
lines changed

5 files changed

+351
-41
lines changed

docs/csharp-examples.md

Lines changed: 207 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@ All examples can be found in the [CodeOnlyTests](../sample/tests/CodeOnlyTests)
66

77
1. [Creating new test classes](#creating-new-test-classes)
88
2. [Testing components without parameters](#testing-components-without-parameters)
9-
3. [Testing components with regular parameters](#testing-components-with-regular-parameters)
9+
3. [Testing components with parameters](#testing-components-with-parameters)
10+
3.1. [Passing new parameters to an already rendered component](#passing-new-parameters-to-an-already-rendered-component)
1011
4. [Testing components with child content](#testing-components-with-child-content)
1112
5. [Testing components with EventCallback parameters](#testing-components-with-eventcallback-parameters)
1213
6. [Testing components with cascading-value parameters](#testing-components-with-cascading-value-parameters)
14+
7. [Testing components that use on IJsRuntime](#testing-components-that-use-on-ijsruntime)
15+
7.1 [Verifying element references](#verifying-element-references)
16+
8. [Testing components with injected dependencies]()
1317

1418
## Creating new test classes
1519

@@ -129,7 +133,7 @@ A few things worth noting about the tests above:
129133

130134
With the _targeted_ version, we cannot guarantee that there are not other changes in other places of the rendered HTML, if that is a concern, use the strict style. If it is not, then the targeted style can lead to simpler test.
131135

132-
## Testing components with regular parameters
136+
## Testing components with parameters
133137

134138
In the following tests we will pass regular parameters to a component under test, e.g. `[Parameter] public SomeType PropName { get; set; }` style properties, where `SomeType` **is not** a `RenderFragment` or a `EventCallback` type.
135139

@@ -190,15 +194,57 @@ As highlighted in the code, I recommend using the [`nameof`](https://docs.micros
190194

191195
The second parameter, `class` is explicitly declared in the `Aside` class. It is instead `Attributes` parameter, that captures all unmatched parameters.
192196

197+
### Passing new parameters to an already rendered component
198+
199+
Sometimes we want to test what happens when a component is re-rendered, possible with new parameters. This can be done using the `cut.Render()` and the `cut.SetParametersAndRender()` methods, for example:
200+
201+
```csharp
202+
[Fact(DisplayName = "Passing new parameters to Aside updates the rendered HTML correctly")]
203+
public void Test002()
204+
{
205+
// Arrange - initial render of Aside
206+
var cut = RenderComponent<Aside>();
207+
208+
// Act - set the Header parameter and re-render the CUT
209+
cut.SetParametersAndRender((nameof(Aside.Header), "HEADER"));
210+
211+
// Assert - Check that we have exactly one change since the first render,
212+
// and that it is an addition to the DOM tree
213+
cut.GetChangesSinceFirstRender()
214+
.ShouldHaveSingleChange()
215+
.ShouldBeAddition("<header>HEADER</header>");
216+
217+
// Arrange - Create a snapshot of the current rendered HTML for later comparisons
218+
cut.TakeSnapshot();
219+
220+
// Act - Set the Header parameter to null again and re-render
221+
cut.SetParametersAndRender((nameof(Aside.Header), null));
222+
223+
// Assert - Check that we have exactly one change since compared with the snapshot we took,
224+
// and that it is an addition to the DOM tree.
225+
cut.GetChangesSinceSnapshot()
226+
.ShouldHaveSingleChange()
227+
.ShouldBeRemoval("<header>HEADER</header>");
228+
}
229+
```
230+
231+
Some notes on `Test002` above:
232+
233+
- The `cut.SetParametersAndRender()` method has the same overloads as the `RenderComponent()` method.
234+
- The `ShouldHaveSingleChange()` method asserts that only a single difference is found by the compare method, and returns that diff object.
235+
- The `ShouldBeAddition()` method verifies that a difference is an addition with the specified content (doing a semantic HTML comparison).
236+
- The `cut.TakeSnapshot()` method saves the current rendered HTML for later comparisons.
237+
- The `cut.GetChangesSinceSnapshot()` compares the current rendered HTML with the one saved by the `TakeSnapshot()` method.
238+
193239
## Testing components with child content
194240

195241
The [Aside.razor](../sample/src/Components/Aside.razor) component listed in the previous section also has a `ChildContent` parameter, so lets add a few tests that passes markup and components to it through that.
196242

197243
```csharp
198244
public class AsideTest : ComponentTestFixture
199245
{
200-
[Fact(DisplayName = "Aside should render child markup content correctly")]
201-
public void Test002()
246+
[Fact(DisplayName = "Aside should render child markup content correctly")]
247+
public void Test003()
202248
{
203249
// Arrange
204250
var content = "<p>I like simple tests and I cannot lie</p>";
@@ -221,7 +267,7 @@ public class AsideTest : ComponentTestFixture
221267
}
222268

223269
[Fact(DisplayName = "Aside should render a child component correctly")]
224-
public void Test003()
270+
public void Test004()
225271
{
226272
// Arrange - set up test data
227273
var outerAsideHeader = "Hello outside";
@@ -255,8 +301,8 @@ public class AsideTest : ComponentTestFixture
255301
}
256302
```
257303

258-
- In `Test002` above we use the `ChildContent(...)` helper method to create a ChildContent parameter and pass that to the `Aside` component.
259-
- The overload, `ChildContent<TComponent>(...)`, used in `Test003`, allows us to create a render fragment that will render a component (of type `TComponent`) with the specified parameters.
304+
- In `Test003` above we use the `ChildContent(...)` helper method to create a ChildContent parameter and pass that to the `Aside` component.
305+
- The overload, `ChildContent<TComponent>(...)`, used in `Test004`, allows us to create a render fragment that will render a component (of type `TComponent`) with the specified parameters.
260306
The `ChildContent<TComponent>(...)` has the same parameter options as the `RenderComponent<TComponent>` method has.
261307

262308
## Testing components with `EventCallback` parameters
@@ -373,3 +419,157 @@ public class ThemedButtonTest : ComponentTestFixture
373419

374420
- `Test002` above uses the `CascadingValue(object value)` helper method to pass an **unnamed** cascading value to the CUT.
375421
- `Test003` above demonstrates how multiple (named) cascading values can be passed to a component under test.
422+
423+
## Testing components that use on `IJsRuntime`
424+
425+
It is not uncommon to have components use Blazor's JSInterop functionality to call JavaScript, e.g. after first render.
426+
427+
To make it easy to mock calls to JavaScript, the library comes with a `IJsRuntime` mocking helper, that allows you to specify return how JSInterop calls should be handled, and to verify that they have happened.
428+
429+
If you have more complex mocking needs, you could look to frameworks like [Moq](https://github.com/Moq).
430+
431+
To help us test the Mock JSRuntime, we have the [WikiSearch.razor](../sample/src/Components/WikiSearch.razor) component, which looks like this:
432+
433+
```cshtml
434+
@inject IJSRuntime jsRuntime
435+
436+
<p>@searchResult</p>
437+
438+
@code {
439+
string searchResult = string.Empty;
440+
441+
// Assumes the following function is available in the DOM
442+
// <script>
443+
// function queryWiki(query) {
444+
// return fetch('https://en.wikipedia.org/w/api.php?origin=*&action=opensearch&search=' + query)
445+
// .then(x => x.text());
446+
// }
447+
// </script>
448+
protected override async Task OnAfterRenderAsync(bool firstRender)
449+
{
450+
if (firstRender)
451+
{
452+
searchResult = await jsRuntime.InvokeAsync<string>("queryWiki", "blazor");
453+
StateHasChanged();
454+
}
455+
}
456+
}
457+
```
458+
459+
The [WikiSearchTest.cs](../sample/test/CodeOnlyTests/Components/WikiSearchTest.cs) looks like this:
460+
461+
```csharp
462+
public class WikiSearchTest : ComponentTestFixture
463+
{
464+
[Fact(DisplayName = "WikiSearch renders an empty P element initially")]
465+
public void Test001()
466+
{
467+
// Arrange
468+
// Registered the MockJsRuntime in "Loose" mode with the service provider used when rendering components.
469+
// JsRuntimeMockMode.Loose is the default. It configures the mock to just return the default
470+
// value for whatever is requested in a InvokeAsync call if no call has explicitly been set up.
471+
var jsMock = Services.AddMockJsRuntime();
472+
473+
// Act - render the WikiSearch component
474+
var cut = RenderComponent<WikiSearch>();
475+
476+
// Assert
477+
// Check that the components initial HTML is as expected
478+
// and that the mock was called with the expected JS identifier and arguments.
479+
cut.ShouldBe("<p></p>");
480+
jsMock.VerifyInvoke("queryWiki").Arguments.Single().ShouldBe("blazor");
481+
}
482+
483+
[Fact(DisplayName = "On first render WikiSearch uses JSInterop to query wiki and display the result")]
484+
public void Test002()
485+
{
486+
// Arrange
487+
// Registered the MockJsRuntime in "strict" mode with the service provider used when rendering components.
488+
// JsRuntimeMockMode.Strict mode configures the mock to throw an error if it receives an InvokeAsync call
489+
// it has not been set up to handle.
490+
var jsMock = Services.AddMockJsRuntime(JsRuntimeMockMode.Strict);
491+
492+
// Set up the mock to handle the expected call
493+
var expectedSearchResult = "SEARCH RESULT";
494+
var plannedInvocation = jsMock.Setup<string>("queryWiki", "blazor");
495+
496+
// Render the WikiSearch and verify that there is no content in the paragraph element
497+
var cut = RenderComponent<WikiSearch>();
498+
cut.Find("p").InnerHtml.ShouldBeEmpty();
499+
500+
// Act
501+
// Use the WaitForNextRender to block until the component has finished re-rendered.
502+
// The plannedInvocation.SetResult will return the result to the component is waiting
503+
// for in its OnAfterRender from the await jsRuntime.InvokeAsync<string>("queryWiki", "blazor") call.
504+
WaitForNextRender(() => plannedInvocation.SetResult(expectedSearchResult));
505+
506+
// Assert
507+
// Verify that the result was received and correct placed in the paragraph element.
508+
cut.Find("p").InnerHtml.ShouldBe(expectedSearchResult);
509+
}
510+
}
511+
```
512+
513+
- `Test001` just injects the mock in "Loose" mode. It means it will only returns a `default(TValue)` for calls to `InvokeAsync<TValue>(...)` it receives. This allows us to test components that expects a `IJsRuntime` to be injected, but where the test we want to perform isn't dependent on it providing any specific return value.
514+
515+
In "Loose" mode it is still possible to call `VerifyInvoke(identifier)` and assert against the expected invocation.
516+
517+
- `Test002` injects and configures the mock in strict mode. That requires us to configure all the expected calls the mock should handle. If it receives a call it has not been configured for, an exception is thrown and the test fails.
518+
519+
- The `WaitForNextRender(Action)` helper method is used to block until a (async) render completes, that the action passed to it has triggered.
520+
In `Test002` we trigger a render by setting the result on the planned invocation, which causes the `await jsRuntime.InvokeAsync<string>("queryWiki", "blazor")` call in the CUT to complete, and the component to trigger a re-render by calling the `StateHasChanged()` method.
521+
522+
### Verifying element references passed to InvokeAsync
523+
524+
If you want to verify that a element reference (`ElementReference`) passed to a IJsRuntime.InvokeAsync call is references the expected DOM element, you can do so with the `ShouldBeElementReferenceTo()` assert helper.
525+
526+
For example, consider the [FocussingInput.razor](../sample/src/Components/FocussingInput.razor) component, which looks like this:
527+
528+
```cshtml
529+
@inject IJSRuntime jsRuntime
530+
531+
<input @ref="_inputRef" @attributes="Attributes" />
532+
533+
@code {
534+
private ElementReference _inputRef;
535+
536+
[Parameter(CaptureUnmatchedValues = true)]
537+
public IReadOnlyDictionary<string, object>? Attributes { get; set; }
538+
539+
protected override async Task OnAfterRenderAsync(bool firstRender)
540+
{
541+
if (firstRender)
542+
{
543+
await jsRuntime.InvokeVoidAsync("document.body.focus.call", _inputRef);
544+
}
545+
}
546+
}
547+
```
548+
549+
The the [FocussingInputTest.cs](../sample/test/CodeOnlyTests/Components/FocussingInputTest.cs) looks like this:
550+
551+
```csharp
552+
public class FocussingInputTest : ComponentTestFixture
553+
{
554+
[Fact(DisplayName = "After first render, the new input field has focus")]
555+
public void Test001()
556+
{
557+
// Arrange - add the IJsRuntime mock
558+
var jsRtMock = Services.AddMockJsRuntime();
559+
560+
// Act - render the FocussingInput component, causing
561+
// the OnAfterRender(firstRender: true) to be called
562+
var cut = RenderComponent<FocussingInput>();
563+
564+
// Assert
565+
// that there is a single call to document.body.focus.call
566+
var invocation = jsRtMock.VerifyInvoke("document.body.focus.call");
567+
// Assert that the invocation received a single argument
568+
// and that it was a reference to the input element.
569+
var expectedReferencedElement = cut.Find("input");
570+
invocation.Arguments.Single().ShouldBeElementReferenceTo(expectedReferencedElement);
571+
}
572+
}
573+
```
574+
575+
The last line verifies that there was a single argument to the invocation, and via the `ShouldBeElementReferenceTo` checks, that the `<input />` was indeed the referenced element.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
@inject IJSRuntime jsRuntime
2+
3+
<input @ref="_inputRef" @attributes="Attributes" />
4+
5+
@code {
6+
private ElementReference _inputRef;
7+
8+
[Parameter(CaptureUnmatchedValues = true)]
9+
public IReadOnlyDictionary<string, object>? Attributes { get; set; }
10+
11+
protected override async Task OnAfterRenderAsync(bool firstRender)
12+
{
13+
if (firstRender)
14+
{
15+
await jsRuntime.InvokeVoidAsync("document.body.focus.call", _inputRef);
16+
}
17+
}
18+
}

sample/tests/CodeOnlyTests/Components/AsideTest.cs

Lines changed: 59 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,34 +8,62 @@
88

99
namespace Egil.RazorComponents.Testing.SampleApp.CodeOnlyTests.Components
1010
{
11-
public class AsideTest : ComponentTestFixture
12-
{
13-
[Fact(DisplayName = "Aside should render header and additional parameters correctly")]
14-
public void Test001()
11+
public class AsideTest : ComponentTestFixture
1512
{
16-
// Arrange
17-
var header = "Hello testers";
18-
var cssClass = "some-class";
13+
[Fact(DisplayName = "Aside should render header and additional parameters correctly")]
14+
public void Test001()
15+
{
16+
// Arrange
17+
var header = "Hello testers";
18+
var cssClass = "some-class";
1919

20-
// Act - render the Aside component with two parameters (passed as pairs of name, value tuples).
21-
// Note the use of the nameof operator to get the name of the Header parameter. This
22-
// helps keeps the test passing if the name of the parameter is refactored.
23-
//
24-
// This is equivalent to the follow Razor code:
25-
//
26-
// <Aside Header="Hello testers" class="some-class">
27-
// </Aside>
28-
var cut = RenderComponent<Aside>(
29-
(nameof(Aside.Header), header),
30-
("class", cssClass)
31-
);
20+
// Act - render the Aside component with two parameters (passed as pairs of name, value tuples).
21+
// Note the use of the nameof operator to get the name of the Header parameter. This
22+
// helps keeps the test passing if the name of the parameter is refactored.
23+
//
24+
// This is equivalent to the follow Razor code:
25+
//
26+
// <Aside Header="Hello testers" class="some-class">
27+
// </Aside>
28+
var cut = RenderComponent<Aside>(
29+
(nameof(Aside.Header), header),
30+
("class", cssClass)
31+
);
3232

33-
// Assert - verify that the rendered HTML from the Aside component matches the expected output.
34-
cut.ShouldBe($@"<aside class=""{cssClass}""><header>{header}</header></aside>");
35-
}
33+
// Assert - verify that the rendered HTML from the Aside component matches the expected output.
34+
cut.ShouldBe($@"<aside class=""{cssClass}""><header>{header}</header></aside>");
35+
}
3636

37-
[Fact(DisplayName = "Aside should render child markup content correctly")]
37+
[Fact(DisplayName = "Passing new parameters to Aside updates the rendered HTML correctly")]
3838
public void Test002()
39+
{
40+
// Arrange - initial render of Aside
41+
var cut = RenderComponent<Aside>();
42+
43+
// Act - set the Header parameter and re-render the CUT
44+
cut.SetParametersAndRender((nameof(Aside.Header), "HEADER"));
45+
46+
// Assert - Check that we have exactly one change since the first render,
47+
// and that it is an addition to the DOM tree
48+
cut.GetChangesSinceFirstRender()
49+
.ShouldHaveSingleChange()
50+
.ShouldBeAddition("<header>HEADER</header>");
51+
52+
// Arrange - Create a snapshot of the current rendered HTML for later comparisons
53+
cut.TakeSnapshot();
54+
55+
// Act - Set the Header parameter to null again and re-render
56+
cut.SetParametersAndRender((nameof(Aside.Header), null));
57+
58+
// Assert - Check that we have exactly one change since compared with the snapshot we took,
59+
// and that it is an addition to the DOM tree.
60+
cut.GetChangesSinceSnapshot()
61+
.ShouldHaveSingleChange()
62+
.ShouldBeRemoval("<header>HEADER</header>");
63+
}
64+
65+
[Fact(DisplayName = "Aside should render child markup content correctly")]
66+
public void Test003()
3967
{
4068
// Arrange
4169
var content = "<p>I like simple tests and I cannot lie</p>";
@@ -57,8 +85,8 @@ public void Test002()
5785
cut.ShouldBe($@"<aside>{content}</aside>");
5886
}
5987

60-
[Fact(DisplayName = "Aside should render a child component correctly")]
61-
public void Test003()
88+
[Fact(DisplayName = "Aside should render a child component correctly")]
89+
public void Test004()
6290
{
6391
// Arrange - set up test data
6492
var outerAsideHeader = "Hello outside";
@@ -83,11 +111,11 @@ public void Test003()
83111

84112
// Assert - verify that the rendered HTML from the Aside component matches the expected output.
85113
cut.ShouldBe($@"<aside>
86-
<header>{outerAsideHeader}</header>
87-
<aside>
88-
<header>{nestedAsideHeader}</header>
89-
</aside>
90-
</aside>");
91-
}
114+
<header>{outerAsideHeader}</header>
115+
<aside>
116+
<header>{nestedAsideHeader}</header>
117+
</aside>
118+
</aside>");
119+
}
92120
}
93121
}

0 commit comments

Comments
 (0)