From 9d63a0d2c6062648c4e1b64e1d802147f0b58181 Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Mon, 6 Jan 2020 12:24:05 +0000 Subject: [PATCH 01/30] Fixed spelling mistake in comment --- src/Components/ContainerComponent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/ContainerComponent.cs b/src/Components/ContainerComponent.cs index 2fd7940bc..6a9092469 100644 --- a/src/Components/ContainerComponent.cs +++ b/src/Components/ContainerComponent.cs @@ -103,7 +103,7 @@ public void Render(RenderFragment renderFragment) // than regular components with child content is not rendered // and available via GetCurrentRenderTreeFrames for the componentId // of the component that had the CascadingValue as a child. - // Thus we call GetComponents recursivly with the CascadingValue's + // Thus we call GetComponents recursively with the CascadingValue's // componentId to see if the TComponent is inside it. result.AddRange(GetComponents(frame.ComponentId)); } From e7b79ac168a725149124d6bc581013a6a901fffc Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Mon, 6 Jan 2020 13:01:13 +0000 Subject: [PATCH 02/30] Reordered parameters to top of TestRenderer --- src/Rendering/TestRenderer.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Rendering/TestRenderer.cs b/src/Rendering/TestRenderer.cs index c5aab9fb3..26b9d9dac 100644 --- a/src/Rendering/TestRenderer.cs +++ b/src/Rendering/TestRenderer.cs @@ -29,6 +29,14 @@ public class TestRenderer : Renderer /// public StructAction? OnRenderingHasComponentUpdates { get; set; } + /// + public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault(); + + /// + /// Gets a task that completes after the next render. + /// + public Task NextRender => _nextRenderTcs.Task; + /// public TestRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory) : base(serviceProvider, loggerFactory) @@ -62,14 +70,6 @@ public int AttachTestRootComponent(IComponent testRootComponent) return task; } - /// - public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault(); - - /// - /// Gets a task that completes after the next render. - /// - public Task NextRender => _nextRenderTcs.Task; - /// protected override void HandleException(Exception exception) { From 457409c5e0b72f57c20b8f58b959f3974612d634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rastislav=20Novotn=C3=BD?= Date: Wed, 15 Jan 2020 17:10:13 +0800 Subject: [PATCH 03/30] Add support for asynchronous Razor tests (#27) * Add support for asyncrhonous Razor tests * Rename Fixture.AsyncTest to TestAsync * Update description of Fixture Co-Authored-By: Egil Hansen --- src/Components/Fixture.cs | 35 +++++++++++++++++++++++++++-- src/Components/TestComponentBase.cs | 26 ++++++++++++++++++--- 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/Components/Fixture.cs b/src/Components/Fixture.cs index d196ec371..4785235b6 100644 --- a/src/Components/Fixture.cs +++ b/src/Components/Fixture.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.AspNetCore.Components; namespace Egil.RazorComponents.Testing @@ -12,8 +13,11 @@ namespace Egil.RazorComponents.Testing public class Fixture : FragmentBase { private Action _setup = NoopTestMethod; + private Func _setupAsync = NoopAsyncTestMethod; private Action _test = NoopTestMethod; + private Func _testAsync = NoopAsyncTestMethod; private IReadOnlyCollection _tests = Array.Empty(); + private IReadOnlyCollection> _testsAsync = Array.Empty>(); /// /// A description or name for the test that will be displayed if the test fails. @@ -21,11 +25,17 @@ public class Fixture : FragmentBase [Parameter] public string? Description { get; set; } /// - /// Gets or sets the setup action to perform before the action - /// and actions are invoked. + /// Gets or sets the setup action to perform before the action, + /// action and and actions are invoked. /// [Parameter] public Action Setup { get => _setup; set => _setup = value ?? NoopTestMethod; } + /// + /// Gets or sets the asynchronous setup action to perform before the action, + /// action and and actions are invoked. + /// + [Parameter] public Func SetupAsync { get => _setupAsync; set => _setupAsync = value ?? NoopAsyncTestMethod; } + /// /// Gets or sets the first test action to invoke, after the action has /// executed (if provided). @@ -35,6 +45,15 @@ public class Fixture : FragmentBase /// [Parameter] public Action Test { get => _test; set => _test = value ?? NoopTestMethod; } + /// + /// Gets or sets the first test action to invoke, after the action has + /// executed (if provided). + /// + /// Use this to assert against the and 's + /// defined in the . + /// + [Parameter] public Func TestAsync { get => _testAsync; set => _testAsync = value ?? NoopAsyncTestMethod; } + /// /// Gets or sets the test actions to invoke, one at the time, in the order they are placed /// into the collection, after the action and the action has @@ -45,6 +64,18 @@ public class Fixture : FragmentBase /// [Parameter] public IReadOnlyCollection Tests { get => _tests; set => _tests = value ?? Array.Empty(); } + /// + /// Gets or sets the test actions to invoke, one at the time, in the order they are placed + /// into the collection, after the action and the action has + /// executed (if provided). + /// + /// Use this to assert against the and 's + /// defined in the . + /// + [Parameter] public IReadOnlyCollection> TestsAsync { get => _testsAsync; set => _testsAsync = value ?? Array.Empty>(); } + private static void NoopTestMethod() { } + + private static Task NoopAsyncTestMethod() => Task.CompletedTask; } } diff --git a/src/Components/TestComponentBase.cs b/src/Components/TestComponentBase.cs index 2969e0031..85e0d70ea 100644 --- a/src/Components/TestComponentBase.cs +++ b/src/Components/TestComponentBase.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Text; +using System.Threading.Tasks; using Egil.RazorComponents.Testing.Asserting; using Egil.RazorComponents.Testing.Diffing; using Microsoft.AspNetCore.Components; @@ -52,12 +53,12 @@ public TestComponentBase() /// in the file and runs their associated tests. /// [Fact(DisplayName = "Razor test runner")] - public void RazorTest() + public async Task RazorTest() { var container = new ContainerComponent(_renderer.Value); container.Render(BuildRenderTree); - ExecuteFixtureTests(container); + await ExecuteFixtureTests(container).ConfigureAwait(false); ExecuteSnapshotTests(container); } @@ -92,7 +93,7 @@ public override void WaitForNextRender(Action renderTrigger, TimeSpan? timeout = base.WaitForNextRender(renderTrigger, timeout); } - private void ExecuteFixtureTests(ContainerComponent container) + private async Task ExecuteFixtureTests(ContainerComponent container) { foreach (var (_, fixture) in container.GetComponents()) { @@ -102,13 +103,20 @@ private void ExecuteFixtureTests(ContainerComponent container) _testContextAdapter.ActivateRazorTestContext(testData); InvokeFixtureAction(fixture, fixture.Setup); + await InvokeFixtureAction(fixture, fixture.SetupAsync).ConfigureAwait(false); InvokeFixtureAction(fixture, fixture.Test); + await InvokeFixtureAction(fixture, fixture.TestAsync).ConfigureAwait(false); foreach (var test in fixture.Tests) { InvokeFixtureAction(fixture, test); } + foreach (var test in fixture.TestsAsync) + { + await InvokeFixtureAction(fixture, test).ConfigureAwait(false); + } + _testContextAdapter.DisposeActiveTestContext(); } } @@ -125,6 +133,18 @@ private static void InvokeFixtureAction(Fixture fixture, Action action) } } + private static async Task InvokeFixtureAction(Fixture fixture, Func action) + { + try + { + await action().ConfigureAwait(false); + } + catch (Exception ex) + { + throw new FixtureFailedException(fixture.Description ?? $"{action.Method.Name} failed:", ex); + } + } + private void ExecuteSnapshotTests(ContainerComponent container) { foreach (var (_, snapshot) in container.GetComponents()) From 057e005e67022fcc10a5789ecab7f9c644fb79f7 Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Tue, 14 Jan 2020 11:40:31 +0000 Subject: [PATCH 04/30] Dotnetconf samples (#33) * Basic example tests * Add missing MarkupMatches overload * Added docs to MockHttp extensions * dotnet conf samples --- .gitignore | 1 + sample/src/Components/Alert.razor | 73 +++++ sample/src/Components/DismissingEventArgs.cs | 20 ++ sample/src/Components/Paragraph.razor | 15 + sample/src/Components/WikiSearch.razor | 2 +- sample/src/Data/Localizer.cs | 30 ++ sample/src/Pages/DotNetConf.razor | 33 +++ sample/src/Pages/_Host.cshtml | 6 + sample/src/Shared/NavMenu.razor | 5 + .../Components/AlertRazorTest.razor | 277 ++++++++++++++++++ .../Components/CascadingValueTest.razor | 1 - .../Components/PassingChildContentTest.razor | 1 - .../Components/ThemedButtonTest.razor | 1 - .../Components/TodoListTest.razor | 1 - .../SnapshotTests/AlertSnapshotTest.razor | 25 ++ sample/tests/Tests/Components/AlertTest.cs | 255 ++++++++++++++++ .../tests/Tests/Components/WikiSearchTest.cs | 8 +- sample/tests/Tests/Pages/CounterTest.cs | 25 ++ sample/tests/_Imports.razor | 3 + .../MarkupMatchesAssertExtensions.cs | 26 ++ src/Mocking/MockHttpExtensions.cs | 16 + ...zorComponents.Testing.Library.Tests.csproj | 1 + .../ClipboardEventDispatchExtensionsTest.cs | 8 +- 23 files changed, 823 insertions(+), 10 deletions(-) create mode 100644 sample/src/Components/Alert.razor create mode 100644 sample/src/Components/DismissingEventArgs.cs create mode 100644 sample/src/Components/Paragraph.razor create mode 100644 sample/src/Data/Localizer.cs create mode 100644 sample/src/Pages/DotNetConf.razor create mode 100644 sample/tests/RazorTestComponents/Components/AlertRazorTest.razor create mode 100644 sample/tests/SnapshotTests/AlertSnapshotTest.razor create mode 100644 sample/tests/Tests/Components/AlertTest.cs diff --git a/.gitignore b/.gitignore index 3e759b75b..7e2388353 100644 --- a/.gitignore +++ b/.gitignore @@ -328,3 +328,4 @@ ASALocalRun/ # MFractors (Xamarin productivity tool) working folder .mfractor/ +*.playlist diff --git a/sample/src/Components/Alert.razor b/sample/src/Components/Alert.razor new file mode 100644 index 000000000..e2c324758 --- /dev/null +++ b/sample/src/Components/Alert.razor @@ -0,0 +1,73 @@ +@inject IJSRuntime jsRuntime + +@if (IsVisible) +{ + +} + +@code { + private const string DefaultCssClass = "alert alert-info alert-dismissible fade"; + + private string ShowCssClass => Dismissing ? "" : "show"; + private string CssClass => AdditionalAttributes?.ContainsKey("class") ?? false + ? $"{DefaultCssClass} {AdditionalAttributes["class"]} {ShowCssClass}" + : $"{DefaultCssClass} {ShowCssClass}"; + + private bool Dismissing { get; set; } + + [CascadingParameter] + private Localizer Localizer { get; set; } = Localizer.Empty; + + [Parameter(CaptureUnmatchedValues = true)] + public IReadOnlyDictionary? AdditionalAttributes { get; set; } + + [Parameter] + public string? Header { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } + + [Parameter] + public EventCallback OnDismissing { get; set; } + + [Parameter] + public EventCallback OnDismissed { get; set; } + + public bool IsVisible { get; private set; } = true; + + public async Task Dismiss() + { + await DismissClicked(); + await InvokeAsync(StateHasChanged); + } + + private async Task DismissClicked() + { + var dismissingArgs = new DismissingEventArgs(this); + + await OnDismissing.InvokeAsync(dismissingArgs); + + if (dismissingArgs.Cancel) + return; + + Dismissing = true; + + await jsRuntime.InvokeVoidAsync("window.transitionFinished"); + + Dismissing = false; + IsVisible = false; + + await OnDismissed.InvokeAsync(this); + } +} \ No newline at end of file diff --git a/sample/src/Components/DismissingEventArgs.cs b/sample/src/Components/DismissingEventArgs.cs new file mode 100644 index 000000000..9d3adee31 --- /dev/null +++ b/sample/src/Components/DismissingEventArgs.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Egil.RazorComponents.Testing.SampleApp.Components +{ + public class DismissingEventArgs + { + public Alert Sender { get; } + + public bool Cancel { get; set; } + + public DismissingEventArgs(Alert sender) + { + Sender = sender; + } + } +} diff --git a/sample/src/Components/Paragraph.razor b/sample/src/Components/Paragraph.razor new file mode 100644 index 000000000..b45f3d415 --- /dev/null +++ b/sample/src/Components/Paragraph.razor @@ -0,0 +1,15 @@ +@if(ChildContent is { }) +{ +

+ @ChildContent +

+} +@code{ + private string CssClass => IsLast ? "mb-0" : string.Empty; + + [Parameter] + public bool IsLast { get; set; } + + [Parameter] + public RenderFragment? ChildContent { get; set; } +} \ No newline at end of file diff --git a/sample/src/Components/WikiSearch.razor b/sample/src/Components/WikiSearch.razor index bb97301b3..81d9e4338 100644 --- a/sample/src/Components/WikiSearch.razor +++ b/sample/src/Components/WikiSearch.razor @@ -1,6 +1,6 @@ @inject IJSRuntime jsRuntime -

@searchResult

+
@searchResult
@code { string searchResult = string.Empty; diff --git a/sample/src/Data/Localizer.cs b/sample/src/Data/Localizer.cs new file mode 100644 index 000000000..d75ed7cd1 --- /dev/null +++ b/sample/src/Data/Localizer.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; + +namespace Egil.RazorComponents.Testing.SampleApp.Data +{ + public class Localizer + { + private readonly Dictionary _localizations + = new Dictionary(); + + public string CultureCode { get; set; } = "en"; + + public string this[string key] + { + get => _localizations.TryGetValue(key, out var localized) + ? localized + : key; + } + + public void Add(string key, string text) + { + _localizations.Add(key, text); + } + + public static Localizer Empty { get; } = new Localizer(); + } +} diff --git a/sample/src/Pages/DotNetConf.razor b/sample/src/Pages/DotNetConf.razor new file mode 100644 index 000000000..664222789 --- /dev/null +++ b/sample/src/Pages/DotNetConf.razor @@ -0,0 +1,33 @@ +@page "/dotnetconf" + + +

.NET Conf: Focus on Blazor is a free, one-day livestream event that features speakers from the community and .NET product teams that are working on building web apps with C# and Blazor. You don't need to use JavaScript anymore with Blazor technology! Blazor lets you build interactive web UIs using C# instead of JavaScript.

+
+ +

Hello .NET Conf

+

+

Alert status:

+
    + @foreach (var msg in dismissMessages) + { +
  1. @msg
  2. + } +
+ +@code { + Alert? alert; + private List dismissMessages = new List(); + + private void DissingAlert(DismissingEventArgs dismissingEvent) + { + dismissMessages.Add($"Alert Dismissing"); + } + + private void DismissedAlert(Alert dismissedAlert) + { + dismissMessages.Add($"Alert Dismissed"); + } +} \ No newline at end of file diff --git a/sample/src/Pages/_Host.cshtml b/sample/src/Pages/_Host.cshtml index 912f0dcb9..1b9311731 100644 --- a/sample/src/Pages/_Host.cshtml +++ b/sample/src/Pages/_Host.cshtml @@ -23,6 +23,12 @@ return fetch('https://en.wikipedia.org/w/api.php?origin=*&action=opensearch&search=' + query) .then(x => x.text()); } + + function transitionFinished() { + return new Promise(function (resolve, reject) { + setTimeout(resolve, 1000); + }); + } diff --git a/sample/src/Shared/NavMenu.razor b/sample/src/Shared/NavMenu.razor index 761d0f999..f165e6fcd 100644 --- a/sample/src/Shared/NavMenu.razor +++ b/sample/src/Shared/NavMenu.razor @@ -27,6 +27,11 @@ Todos + diff --git a/sample/tests/RazorTestComponents/Components/AlertRazorTest.razor b/sample/tests/RazorTestComponents/Components/AlertRazorTest.razor new file mode 100644 index 000000000..2223bbdd6 --- /dev/null +++ b/sample/tests/RazorTestComponents/Components/AlertRazorTest.razor @@ -0,0 +1,277 @@ +@inherits TestComponentBase + +@code { + MockJsRuntimeInvokeHandler MockJsRuntime { get; set; } + + void Setup() + { + MockJsRuntime = Services.AddMockJsRuntime(); + } +} + + + + + + + + + +@code { + void Test001() + { + // Arrange and act + var cut = GetComponentUnderTest(); + + // Assert + IRenderedFragment expected = GetFragment("expected"); + cut.MarkupMatches(expected); + } +} + + + + + + @testContent + + + + + + @testContent + + + +@code { + string testContent = ".NET Conf: Focus on Blazor is a free ..."; + void Test002() + { + // Arrange and act + var cut = GetComponentUnderTest(); + + // Assert + var actual = cut.Find(".alert-content > p").TextContent.Trim(); + Assert.Equal(testContent, actual); + + IRenderedFragment expected = GetFragment(); + cut.Find(".alert-content > p").MarkupMatches(expected); + } +} + + + + + @testContent + + + +@code { + void Test003() + { + // Arrange and act + var cut = GetComponentUnderTest(); + + // Assert + // Verify by looking at the content and handling whitespace + var actual = cut.Find(".alert-content").TextContent.Trim(); + Assert.Equal(testContent, actual); + + // Verify using semantic comparison of all child nodes inside content + cut.Find(".alert-content") + .ChildNodes + .MarkupMatches(testContent); + } +} + + + + + + +

@headerText

+
+
+@code { + string headerText = "It is time to focus on Blazor"; + void Test004() + { + // Arrange and Act + var cut = GetComponentUnderTest(); + + // Assert + string expected = cut.Find(".alert-heading").TextContent; + Assert.Equal(headerText, expected); + + var expectedMarkup = GetFragment(); + cut.Find(".alert-heading").MarkupMatches(expectedMarkup); + } +} + + + + + + + + + + +

@localizer[headerKey]

+
+
+@code { + Localizer localizer = new Localizer() { CultureCode = "Yoda" }; + string headerKey = "alert-heading"; + + void Given_Header_and_Localizer_Alert_Uses_Lozalizer_to_lozalize_Header_text() + { + // Arrange and act + localizer.Add(headerKey, "Time to focus on Blazor it is."); + var cut = GetComponentUnderTest(); + + // Assert + var expected = cut.Find(".alert-heading").TextContent; + Assert.Equal(localizer[headerKey], expected); + + var expectedMarkup = GetFragment(); + cut.Find(".alert-heading").MarkupMatches(expectedMarkup); + } +} + + + + + + + + + +@code { + DismissingEventArgs? dismissingEvent = default; + Alert? dismissedAlert = default; + + void Test006() + { + // Arrange + var cut = GetComponentUnderTest(); + + // Act + cut.Find("button").Click(); + + // Assert + Assert.NotNull(dismissingEvent); + Assert.Equal(cut.Instance, dismissedAlert); + } +} + + + + + + + + + +@code { + void Test007() + { + // arrange + var plannedInvocation = MockJsRuntime.Setup("window.transitionFinished"); + var cut = GetComponentUnderTest(); + + // Act + cut.Find("button").Click(); + + // Assert that css transition has started + Assert.DoesNotContain("show", cut.Find(".alert").ClassList); + + // Act - complete + WaitForNextRender(() => + { + plannedInvocation.SetResult(default!); + }); + + // Assert that all markup is gone + cut.MarkupMatches(string.Empty); + } +} + + + + + + + +@code { + Alert? testAlert; + void Test008() + { + // arrange + var cut = GetComponentUnderTest(); + + // Act + testAlert?.Dismiss().Wait(); + + // Assert that all markup is gone + cut.MarkupMatches(string.Empty); + Assert.False(cut.Instance.IsVisible); + } +} + + + + + + + .NET Conf: Focus on Blazor is a free ... + + + + + + + + +@code { + Localizer localizer2 = new Localizer() { CultureCode = "Yoda" }; + + void Test009() + { + // Arrange + localizer2.Add("alert-heading", "Time to focus on Blazor it is."); + + // Act + var cut = GetComponentUnderTest(); + + // Assert + var expected = GetFragment(); + cut.MarkupMatches(expected); + } +} \ No newline at end of file diff --git a/sample/tests/RazorTestComponents/Components/CascadingValueTest.razor b/sample/tests/RazorTestComponents/Components/CascadingValueTest.razor index cd5cbc95a..11bef2596 100644 --- a/sample/tests/RazorTestComponents/Components/CascadingValueTest.razor +++ b/sample/tests/RazorTestComponents/Components/CascadingValueTest.razor @@ -1,5 +1,4 @@ @inherits TestComponentBase -@using Shouldly diff --git a/sample/tests/RazorTestComponents/Components/PassingChildContentTest.razor b/sample/tests/RazorTestComponents/Components/PassingChildContentTest.razor index fa4a7e8df..a47a22b82 100644 --- a/sample/tests/RazorTestComponents/Components/PassingChildContentTest.razor +++ b/sample/tests/RazorTestComponents/Components/PassingChildContentTest.razor @@ -1,5 +1,4 @@ @inherits TestComponentBase -@using Shouldly diff --git a/sample/tests/RazorTestComponents/Components/ThemedButtonTest.razor b/sample/tests/RazorTestComponents/Components/ThemedButtonTest.razor index 4ac546712..4dc524c95 100644 --- a/sample/tests/RazorTestComponents/Components/ThemedButtonTest.razor +++ b/sample/tests/RazorTestComponents/Components/ThemedButtonTest.razor @@ -1,5 +1,4 @@ @inherits TestComponentBase -@using Shouldly diff --git a/sample/tests/RazorTestComponents/Components/TodoListTest.razor b/sample/tests/RazorTestComponents/Components/TodoListTest.razor index 44b2f6467..8c378ea4c 100644 --- a/sample/tests/RazorTestComponents/Components/TodoListTest.razor +++ b/sample/tests/RazorTestComponents/Components/TodoListTest.razor @@ -1,5 +1,4 @@ @inherits TestComponentBase -@using Xunit + + + + .NET Conf: Focus on Blazor is a free ... + + + + + + + \ No newline at end of file diff --git a/sample/tests/Tests/Components/AlertTest.cs b/sample/tests/Tests/Components/AlertTest.cs new file mode 100644 index 000000000..9067e065b --- /dev/null +++ b/sample/tests/Tests/Components/AlertTest.cs @@ -0,0 +1,255 @@ +using System; +using System.Threading.Tasks; +using Egil.RazorComponents.Testing.Asserting; +using Egil.RazorComponents.Testing.EventDispatchExtensions; +using Egil.RazorComponents.Testing.SampleApp.Components; +using Egil.RazorComponents.Testing.SampleApp.Data; +using Microsoft.AspNetCore.Authentication; +using Xunit; + +namespace Egil.RazorComponents.Testing.SampleApp.Tests.Components +{ + public class AlertTest2 : ComponentTestFixture + { + MockJsRuntimeInvokeHandler MockJsRuntime { get; } + + public AlertTest2() + { + MockJsRuntime = Services.AddMockJsRuntime(); + } + + [Fact(DisplayName = "Given no parameters, " + + "Alert renders its basic markup, " + + "including dismiss button")] + public void Test001() + { + // Arrange and Act + var cut = RenderComponent(); + + // Assert + cut.MarkupMatches( + $@"
+
+ +
"); + } + + [Fact(DisplayName = "Given a component as Child Content, " + + "Alert renders it inside its element")] + public void Test002() + { + // Arrange and act + var content = ".NET Conf: Focus on Blazor is a free ..."; + var cut = RenderComponent( + ChildContent( + (nameof(Paragraph.IsLast), true), + ChildContent(content) + ) + ); + + // Assert + // Verify by looking at the text content. + var actual = cut.Find(".alert-content > p").TextContent.Trim(); + Assert.Equal(content, actual); + + // Verify by creating the expected component + var expected = RenderComponent( + (nameof(Paragraph.IsLast), true), + ChildContent(content) + ); + cut.Find(".alert-content > p").MarkupMatches(expected); + + // This verification is actually testing + var expectedMarkup = $@"

{content}

"; + cut.Find(".alert-content > p").MarkupMatches(expectedMarkup); + } + + [Fact(DisplayName = "Given Child Content as input, " + + "Alert renders it inside its element")] + public void Test003() + { + // Arrange and act + var content = ".NET Conf: Focus on Blazor is a free ..."; + var cut = RenderComponent( + ChildContent(content) + ); + + // Assert + // Verify by looking at the content and handling whitespace + var actual = cut.Find(".alert-content").TextContent.Trim(); + Assert.Equal(content, actual); + + // Verify using semantic comparison of all child nodes inside content + cut.Find(".alert-content") + .ChildNodes + .MarkupMatches(content); + } + + [Fact(DisplayName = "Given a Header as input, " + + "Alert renders the header text in the expected element")] + public void Test004() + { + // Arrange + var headerText = "It is time to focus on Blazor"; + + // Act + var cut = RenderComponent( + (nameof(Alert.Header), headerText) + ); + + // Assert + string expected = cut.Find(".alert-heading").TextContent; + Assert.Equal(headerText, expected); + + var expectedMarkup = $@"

{headerText}

"; + cut.Find(".alert-heading").MarkupMatches(expectedMarkup); + } + + [Fact(DisplayName = "Given a Header and Localizer as input, " + + "Alert uses Localizer to localize Header text")] + public void Test005() + { + // Arrange + var headerKey = "alert-heading"; + var localizer = new Localizer() { CultureCode = "Yoda" }; + localizer.Add(headerKey, "Time to focus on Blazor it is."); + + // Act + var cut = RenderComponent( + (nameof(Alert.Header), headerKey), + CascadingValue(localizer) + ); + + // Assert + var expected = cut.Find(".alert-heading").TextContent; + Assert.Equal(localizer[headerKey], expected); + + var expectedMarkup = $@"

{localizer[headerKey]}

"; + cut.Find(".alert-heading").MarkupMatches(expectedMarkup); + } + + [Fact(DisplayName = "When dismiss button is clicked, " + + "Alert triggers expected callbacks")] + public void Test006() + { + // Arrange + DismissingEventArgs? dismissingEvent = default; + Alert? dismissedAlert = default; + var cut = RenderComponent( + EventCallback(nameof(Alert.OnDismissing), arg => dismissingEvent = arg), + EventCallback(nameof(Alert.OnDismissed), alert => dismissedAlert = alert) + ); + + // Act + cut.Find("button").Click(); + + // Assert + Assert.NotNull(dismissingEvent); + Assert.Equal(cut.Instance, dismissedAlert); + } + + [Fact(DisplayName = "Full uncanceled Dismiss work flow executes as expected")] + public void Test() + { + // Arrange + var mockJsRuntime = Services.AddMockJsRuntime(); + var plannedInvocation = mockJsRuntime.Setup("window.transitionFinished"); + + DismissingEventArgs? dismissingEvent = default; + Alert? dismissedAlert = default; + + var cut = RenderComponent( + EventCallback(nameof(Alert.OnDismissing), arg => dismissingEvent = arg), + EventCallback(nameof(Alert.OnDismissed), alert => dismissedAlert = alert) + ); + + // Act + cut.Find("button").Click(); + + // Assert + Assert.DoesNotContain("show", cut.Find(".alert").ClassList); + Assert.NotNull(dismissingEvent); + + // Act + WaitForNextRender(() => + { + plannedInvocation.SetResult(default!); + }); + + // Assert + cut.MarkupMatches(string.Empty); + Assert.NotNull(dismissedAlert); + } + + [Fact(DisplayName = "When dismiss button is clicked, " + + "Alert trigger css animation " + + "and finally removes all markup")] + public void Test007() + { + // Arrange + var plannedInvocation = MockJsRuntime.Setup("window.transitionFinished"); + var cut = RenderComponent(); + + // Act - click the button + cut.Find("button").Click(); + + // Assert that css transition has started + Assert.DoesNotContain("show", cut.Find(".alert").ClassList); + + // Act - complete + WaitForNextRender(() => + { + plannedInvocation.SetResult(default!); + }); + + // Assert that all markup is gone + cut.MarkupMatches(string.Empty); + } + + [Fact(DisplayName = "Alert can be dismissed via Dismiss() mehod")] + public async Task Test008() + { + // Arrange + var cut = RenderComponent(); + + // Act + await cut.Instance.Dismiss(); + + // Assert that all markup is gone + cut.MarkupMatches(string.Empty); + Assert.False(cut.Instance.IsVisible); + } + + [Fact(DisplayName = "Alert renders correctly when all input is provided")] + public void Test009() + { + // Arrange + var content = "NET Conf: Focus on Blazor is a free ..."; + var headerKey = "alert-heading"; + var localizer = new Localizer() { CultureCode = "Yoda" }; + localizer.Add(headerKey, "Time to focus on Blazor it is."); + + // Act + var cut = RenderComponent( + (nameof(Alert.Header), headerKey), + CascadingValue(localizer), + EventCallback(nameof(Alert.OnDismissing), arg => { }), + EventCallback(nameof(Alert.OnDismissed), arg => { }), + ChildContent(content) + ); + + // Assert + var expected = $@"
+

{localizer[headerKey]}

+
{content}
+ +
"; + + cut.MarkupMatches(expected); + } + } +} diff --git a/sample/tests/Tests/Components/WikiSearchTest.cs b/sample/tests/Tests/Components/WikiSearchTest.cs index f3cbd3f46..2d5bfd100 100644 --- a/sample/tests/Tests/Components/WikiSearchTest.cs +++ b/sample/tests/Tests/Components/WikiSearchTest.cs @@ -12,7 +12,7 @@ namespace Egil.RazorComponents.Testing.SampleApp.CodeOnlyTests.Components { public class WikiSearchTest : ComponentTestFixture { - [Fact(DisplayName = "WikiSearch renders an empty P element initially")] + [Fact(DisplayName = "WikiSearch renders an empty PRE element initially")] public void Test001() { // Arrange @@ -27,7 +27,7 @@ public void Test001() // Assert // Check that the components initial HTML is as expected // and that the mock was called with the expected JS identifier and arguments. - cut.MarkupMatches("

"); + cut.MarkupMatches(@"
");
             jsMock.VerifyInvoke("queryWiki").Arguments.Single().ShouldBe("blazor");
         }
 
@@ -46,7 +46,7 @@ public void Test002()
 
             // Render the WikiSearch and verify that there is no content in the paragraph element
             var cut = RenderComponent();
-            cut.Find("p").InnerHtml.ShouldBeEmpty();
+            cut.Find("pre").InnerHtml.Trim().ShouldBeEmpty();
 
             // Act
             // Use the WaitForNextRender to block until the component has finished re-rendered.
@@ -56,7 +56,7 @@ public void Test002()
 
             // Assert
             // Verify that the result was received and correct placed in the paragraph element.
-            cut.Find("p").InnerHtml.ShouldBe(expectedSearchResult);
+            cut.Find("pre").InnerHtml.Trim().ShouldBe(expectedSearchResult);
         }
     }
 }
diff --git a/sample/tests/Tests/Pages/CounterTest.cs b/sample/tests/Tests/Pages/CounterTest.cs
index 897cacd78..6a8860b50 100644
--- a/sample/tests/Tests/Pages/CounterTest.cs
+++ b/sample/tests/Tests/Pages/CounterTest.cs
@@ -3,6 +3,7 @@
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
+using AngleSharp.Diffing.Core;
 using Egil.RazorComponents.Testing.Asserting;
 using Egil.RazorComponents.Testing.EventDispatchExtensions;
 using Egil.RazorComponents.Testing.SampleApp.Pages;
@@ -72,5 +73,29 @@ public void ClickingButtonIncreasesCountTargeted()
             cut.Find("button").Click();
             cut.Find("p").TextContent.Trim().ShouldBe("Current count: 2");
         }
+
+        [Fact(DisplayName = "Clicking Counter button increases count")]
+        public void Test()
+        {
+            // Arrange
+            var cut = RenderComponent();
+
+            // Act
+            cut.Find("button").Click();
+
+            // Assert
+            IReadOnlyList diffs = cut.GetChangesSinceFirstRender();
+            diffs.ShouldHaveSingleTextChange("Current count: 1");
+
+            Assert.Equal("Current count: 1", cut.Find("p").TextContent.Trim());
+
+            // Act
+            cut.SaveSnapshot();
+            cut.Find("button").Click();
+
+            // Assert
+            diffs = cut.GetChangesSinceSnapshot();
+            diffs.ShouldHaveSingleTextChange("Current count: 2");
+        }
     }
 }
diff --git a/sample/tests/_Imports.razor b/sample/tests/_Imports.razor
index bef6d8fcf..4b2a8d793 100644
--- a/sample/tests/_Imports.razor
+++ b/sample/tests/_Imports.razor
@@ -1,10 +1,13 @@
 @using Microsoft.AspNetCore.Components.Web
+
 @using Egil.RazorComponents.Testing
 @using Egil.RazorComponents.Testing.EventDispatchExtensions
 @using Egil.RazorComponents.Testing.Asserting
+
 @using Egil.RazorComponents.Testing.SampleApp
 @using Egil.RazorComponents.Testing.SampleApp.Data
 @using Egil.RazorComponents.Testing.SampleApp.Components
 @using Egil.RazorComponents.Testing.SampleApp.Pages
+
 @using Xunit
 @using Shouldly
diff --git a/src/Asserting/MarkupMatchesAssertExtensions.cs b/src/Asserting/MarkupMatchesAssertExtensions.cs
index fd506515c..87bf3a483 100644
--- a/src/Asserting/MarkupMatchesAssertExtensions.cs
+++ b/src/Asserting/MarkupMatchesAssertExtensions.cs
@@ -81,6 +81,32 @@ public static void MarkupMatches(this INode actual, IRenderedFragment expected,
             actual.MarkupMatches(expected.GetNodes(), userMessage);
         }
 
+        /// 
+        /// Verifies that the   matches
+        /// the  markup, using the  
+        /// type.
+        /// 
+        /// Thrown when the  markup does not match the  markup.
+        /// The node to verify.
+        /// The expected markup.
+        /// A custom user message to display in case the verification fails.
+        public static void MarkupMatches(this INode actual, string expected, string? userMessage = null)
+        {
+            if (actual is null) throw new ArgumentNullException(nameof(actual));
+
+            INodeList expectedNodes;
+            if (actual.GetHtmlParser() is { } parser)
+            {
+                expectedNodes = parser.Parse(expected);
+            }
+            else
+            {
+                using var newParser = new TestHtmlParser();
+                expectedNodes = newParser.Parse(expected);
+            }
+            MarkupMatches(actual, expectedNodes, userMessage);
+        }
+
         /// 
         /// Verifies that the   matches
         /// the  markup, using the  
diff --git a/src/Mocking/MockHttpExtensions.cs b/src/Mocking/MockHttpExtensions.cs
index b04b63538..79e706b45 100644
--- a/src/Mocking/MockHttpExtensions.cs
+++ b/src/Mocking/MockHttpExtensions.cs
@@ -9,8 +9,17 @@
 
 namespace Egil.RazorComponents.Testing
 {
+    /// 
+    /// Helper methods for adding a Mock HTTP client to a service provider.
+    /// 
     public static class MockHttpExtensions
     {
+        /// 
+        /// Create a  and adds it to the 
+        ///  as a .
+        /// 
+        /// 
+        /// The .
         public static MockHttpMessageHandler AddMockHttp(this TestServiceProvider serviceProvider)
         {
             if (serviceProvider is null) throw new ArgumentNullException(nameof(serviceProvider));
@@ -22,6 +31,13 @@ public static MockHttpMessageHandler AddMockHttp(this TestServiceProvider servic
             return mockHttp;
         }
 
+        /// 
+        /// Configure a  to capture requests to 
+        /// a .
+        /// 
+        /// 
+        /// Url of requests to capture.
+        /// A  that can be used to send a response to a captured request.
         [SuppressMessage("Reliability", "CA2008:Do not create tasks without passing a TaskScheduler", Justification = "")]
         [SuppressMessage("Design", "CA1054:Uri parameters should not be strings", Justification = "")]
         public static TaskCompletionSource Capture(this MockHttpMessageHandler handler, string url)
diff --git a/tests/Egil.RazorComponents.Testing.Library.Tests.csproj b/tests/Egil.RazorComponents.Testing.Library.Tests.csproj
index 74ad8a53d..304d81e50 100644
--- a/tests/Egil.RazorComponents.Testing.Library.Tests.csproj
+++ b/tests/Egil.RazorComponents.Testing.Library.Tests.csproj
@@ -9,6 +9,7 @@
   
     
     
+    
     
     
     
diff --git a/tests/EventDispatchExtensions/ClipboardEventDispatchExtensionsTest.cs b/tests/EventDispatchExtensions/ClipboardEventDispatchExtensionsTest.cs
index 05ca5afdb..e3b6f8f05 100644
--- a/tests/EventDispatchExtensions/ClipboardEventDispatchExtensionsTest.cs
+++ b/tests/EventDispatchExtensions/ClipboardEventDispatchExtensionsTest.cs
@@ -1,7 +1,13 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 using System.Reflection;
+using System.Threading;
 using System.Threading.Tasks;
+using Egil.RazorComponents.Testing.SampleComponents;
+using Egil.RazorComponents.Testing.SampleComponents.Data;
 using Microsoft.AspNetCore.Components.Web;
+using Moq;
+using Shouldly;
 using Xunit;
 
 namespace Egil.RazorComponents.Testing.EventDispatchExtensions

From 51d3a0fcd6f16ab93dff7bd6b852d6e8299573fc Mon Sep 17 00:00:00 2001
From: Egil Hansen 
Date: Thu, 16 Jan 2020 12:44:32 +0000
Subject: [PATCH 05/30] Added unit tests of CompareTo, Generic and Collection
 assert extensions.

---
 ...sions.cs => CollectionAssertExtensions.cs} |  53 +++++++-
 src/Asserting/HtmlEqualException.cs           |   8 --
 .../CompareToDiffingExtensionsTest.cs         |  90 +++++++++++++
 tests/Asserting/GenericAssertExtensions.cs    |  87 ++++++++++++
 .../GenericCollectionAssertExtensionsTest.cs  | 124 ++++++++++++++++++
 5 files changed, 353 insertions(+), 9 deletions(-)
 rename src/Asserting/{GenericAssertExtensions.cs => CollectionAssertExtensions.cs} (55%)
 create mode 100644 tests/Asserting/CompareToDiffingExtensionsTest.cs
 create mode 100644 tests/Asserting/GenericAssertExtensions.cs
 create mode 100644 tests/Asserting/GenericCollectionAssertExtensionsTest.cs

diff --git a/src/Asserting/GenericAssertExtensions.cs b/src/Asserting/CollectionAssertExtensions.cs
similarity index 55%
rename from src/Asserting/GenericAssertExtensions.cs
rename to src/Asserting/CollectionAssertExtensions.cs
index c6a0f007f..3d5198d10 100644
--- a/src/Asserting/GenericAssertExtensions.cs
+++ b/src/Asserting/CollectionAssertExtensions.cs
@@ -8,10 +8,61 @@
 namespace Egil.RazorComponents.Testing.Asserting
 {
     /// 
-    /// Generic test assertions
+    /// Collection test assertions
     /// 
     public static class GenericAssertExtensions
     {
+        /// 
+        /// Verifies that  is not null
+        /// and returns  again.
+        /// 
+        /// Returns  if it is not null.
+        public static T ShouldNotBeNull([NotNullIfNotNull("actual")]this T? actual) where T : class
+        {
+            if (actual is null)
+                throw new XunitException($"{nameof(ShouldNotBeNull)}() Failure");
+            return actual;
+        }
+
+        /// 
+        /// Verifies that  is not null
+        /// and returns  again.
+        /// 
+        /// Returns  if it is not null.
+        public static T ShouldNotBeNull([NotNullIfNotNull("actual")]this T? actual) where T : struct
+        {
+            if (actual is null)
+                throw new XunitException($"{nameof(ShouldNotBeNull)}() Failure");
+            return actual.Value;
+        }
+
+        /// 
+        /// Verifies that a nullable  is not null
+        /// and of type .
+        /// 
+        /// Returns  as .
+        public static T ShouldBeOfType([NotNullIfNotNull("actual")]this object? actual)
+        {
+            return Assert.IsType(actual);
+        }
+
+        /// 
+        /// Verifies that a non nullable struct is the same as its nullable counter part.
+        /// 
+        public static void ShouldBe(this T actual, T? expected)
+             where T : struct
+        {
+            Assert.Equal(expected, actual);
+        }
+    }
+
+    /// 
+    /// Collection test assertions
+    /// 
+    public static class CollectionAssertExtensions
+    {
+
+
         /// 
         /// Verifies that a collection contains exactly a given number of elements, which
         /// meet the criteria provided by the element inspectors.
diff --git a/src/Asserting/HtmlEqualException.cs b/src/Asserting/HtmlEqualException.cs
index 4865b58c5..0d7c3c878 100644
--- a/src/Asserting/HtmlEqualException.cs
+++ b/src/Asserting/HtmlEqualException.cs
@@ -16,14 +16,6 @@ namespace Xunit.Sdk
     [SuppressMessage("Design", "CA1032:Implement standard exception constructors", Justification = "")]
     public class HtmlEqualException : AssertActualExpectedException
     {
-        /// 
-        /// Creates an instance of the  type.
-        /// 
-        public HtmlEqualException(IEnumerable diffs, IMarkupFormattable expected, IMarkupFormattable actual, string? userMessage, Exception innerException)
-            : base(PrintHtml(expected), PrintHtml(actual), CreateUserMessage(diffs, userMessage), "Expected HTML", "Actual HTML", innerException)
-        {
-        }
-
         /// 
         /// Creates an instance of the  type.
         /// 
diff --git a/tests/Asserting/CompareToDiffingExtensionsTest.cs b/tests/Asserting/CompareToDiffingExtensionsTest.cs
new file mode 100644
index 000000000..7eb34e870
--- /dev/null
+++ b/tests/Asserting/CompareToDiffingExtensionsTest.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using AngleSharp.Dom;
+using Egil.RazorComponents.Testing.SampleComponents;
+using Egil.RazorComponents.Testing.TestUtililities;
+using Shouldly;
+using Xunit;
+
+namespace Egil.RazorComponents.Testing.Asserting
+{
+    public class CompareToDiffingExtensionsTest : ComponentTestFixture
+    {
+        /// 
+        /// Returns an array of arrays containing:
+        /// (MethodInfo methodInfo, string argName, object[] methodArgs)
+        /// 
+        /// 
+        public static IEnumerable GetCompareToMethods()
+        {
+            var methods = typeof(CompareToExtensions)
+                .GetMethods()
+                .Where(x => x.Name.Equals(nameof(CompareToExtensions.CompareTo), StringComparison.Ordinal))
+                .ToList();
+
+            foreach (var method in methods)
+            {
+                var p1Info = method.GetParameters()[0];
+                var p2Info = method.GetParameters()[1];
+                object p1 = p1Info.ParameterType.ToMockInstance();
+                object p2 = p2Info.ParameterType.ToMockInstance();
+
+                yield return new object[] { method, p1Info.Name!, new object[] { null!, p2! } };
+                yield return new object[] { method, p2Info.Name!, new object[] { p1!, null! } };
+            }
+        }
+
+        [Theory(DisplayName = "CompareTo null values throws")]
+        [MemberData(nameof(GetCompareToMethods))]
+        public void Test001(MethodInfo methodInfo, string argName, object[] args)
+        {
+            Should.Throw(() => methodInfo.Invoke(null, args))
+                .InnerException
+                .ShouldBeOfType()
+                .ParamName.ShouldBe(argName);
+        }
+
+        [Fact(DisplayName = "CompareTo with rendered fragment and string")]
+        public void Test002()
+        {
+            var rf1 = RenderComponent((nameof(Simple1.Header), "FOO"));
+            var rf2 = RenderComponent((nameof(Simple1.Header), "BAR"));
+
+            rf1.CompareTo(rf2.GetMarkup()).Count.ShouldBe(1);
+        }
+
+        [Fact(DisplayName = "CompareTo with rendered fragment and rendered fragment")]
+        public void Test003()
+        {
+            var rf1 = RenderComponent((nameof(Simple1.Header), "FOO"));
+            var rf2 = RenderComponent((nameof(Simple1.Header), "BAR"));
+
+            rf1.CompareTo(rf2).Count.ShouldBe(1);
+        }
+
+        [Fact(DisplayName = "CompareTo with INode and INodeList")]
+        public void Test004()
+        {
+            var rf1 = RenderComponent((nameof(Simple1.Header), "FOO"));
+            var rf2 = RenderComponent((nameof(Simple1.Header), "BAR"));
+
+            var elm = rf1.Find("h1");            
+            elm.CompareTo(rf2.GetNodes()).Count.ShouldBe(1);
+        }
+
+
+        [Fact(DisplayName = "CompareTo with INodeList and INode")]
+        public void Test005()
+        {
+            var rf1 = RenderComponent((nameof(Simple1.Header), "FOO"));
+            var rf2 = RenderComponent((nameof(Simple1.Header), "BAR"));
+
+            var elm = rf1.Find("h1");            
+            rf2.GetNodes().CompareTo(elm).Count.ShouldBe(1);
+        }
+    }
+}
diff --git a/tests/Asserting/GenericAssertExtensions.cs b/tests/Asserting/GenericAssertExtensions.cs
new file mode 100644
index 000000000..1a28aaabd
--- /dev/null
+++ b/tests/Asserting/GenericAssertExtensions.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Collections.Generic;
+using Shouldly;
+using Xunit;
+using Xunit.Sdk;
+
+namespace Egil.RazorComponents.Testing.Asserting
+{
+    public class GenericAssertExtensions
+    {
+        [Fact(DisplayName = "ShouldNotBeNull throws exception when input is class and null")]
+        public void Test001()
+        {
+            Exception? exception = null;
+            object? input = null;
+            try
+            {
+                input.ShouldNotBeNull();
+            }
+            catch (Exception ex)
+            {
+                exception = ex;
+            }
+
+            var actual = exception.ShouldBeOfType();
+            actual.Message.ShouldContain("ShouldNotBeNull");
+        }
+
+        [Fact(DisplayName = "ShouldNotBeNull throws exception when input is struct and null")]
+        public void Test002()
+        {
+            Exception? exception = null;
+            int? input = null;
+            try
+            {
+                input.ShouldNotBeNull();
+            }
+            catch (Exception ex)
+            {
+                exception = ex;
+            }
+
+            var actual = exception.ShouldBeOfType();
+            actual.Message.ShouldContain("ShouldNotBeNull");
+        }
+
+        [Fact(DisplayName = "ShouldNotBeNull returns input is class and it is not null")]
+        public void Test003()
+        {
+            object? input = new object();
+            var output = input.ShouldNotBeNull();
+            output.ShouldBe(input);
+        }
+
+        [Fact(DisplayName = "ShouldNotBeNull returns input is struct and it is not null")]
+        public void Test004()
+        {
+            int? input = 42;
+            var output = input.ShouldNotBeNull();
+            output.ShouldBe(input);
+        }
+
+        [Fact(DisplayName = "ShouldBeOfType throws when actual is a different type")]
+        public void Test005()
+        {
+            Exception? exception = null;
+            try
+            {
+                "foo".ShouldBeOfType();
+            }
+            catch (Exception ex)
+            {
+                exception = ex;
+            }
+
+            Assert.IsType(exception);
+        }
+
+        [Fact(DisplayName = "ShouldBeOfType returns input when type is as expected")]
+        public void Test006()
+        {
+            object? input = "foo";
+            var actual = input.ShouldBeOfType();
+            actual.ShouldBe(input);
+        }
+    }
+}
diff --git a/tests/Asserting/GenericCollectionAssertExtensionsTest.cs b/tests/Asserting/GenericCollectionAssertExtensionsTest.cs
new file mode 100644
index 000000000..37e504d85
--- /dev/null
+++ b/tests/Asserting/GenericCollectionAssertExtensionsTest.cs
@@ -0,0 +1,124 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Shouldly;
+using Xunit;
+using Xunit.Sdk;
+
+namespace Egil.RazorComponents.Testing.Asserting
+{
+    public class GenericCollectionAssertExtensionsTest
+    {
+        [Fact(DisplayName = "ShouldAllBe for Action throws CollectionException when " +
+                            "the number of element inspectors does not match the " +
+                            "number of items in the collection")]
+        public void Test001()
+        {
+            Exception? exception = null;
+
+            var collection = new string[] { "foo", "bar" };
+            try
+            {
+                collection.ShouldAllBe(x => { });
+            }
+            catch (Exception ex)
+            {
+                exception = ex;
+            };
+
+            var actual = exception.ShouldNotBeNull().ShouldBeOfType();
+            actual.ActualCount.ShouldBe(collection.Length);
+            actual.ExpectedCount.ShouldBe(1);
+        }
+
+        [Fact(DisplayName = "ShouldAllBe for Action throws CollectionException if one of " +
+                            "the element inspectors throws")]
+        public void Test002()
+        {
+            Exception? exception = null;
+
+            var collection = new string[] { "foo", "bar" };
+            try
+            {
+                collection.ShouldAllBe(x => { }, x => throw new Exception());
+            }
+            catch (Exception ex)
+            {
+                exception = ex;
+            };
+
+            var actual = exception.ShouldNotBeNull().ShouldBeOfType();
+            actual.IndexFailurePoint.ShouldBe(1);
+        }
+
+        [Fact(DisplayName = "ShouldAllBe for Action throws CollectionException when " +
+                            "the number of element inspectors does not match the " +
+                            "number of items in the collection")]
+        public void Test003()
+        {
+            Exception? exception = null;
+
+            var collection = new string[] { "foo", "bar" };
+            try
+            {
+                collection.ShouldAllBe((x, i) => { });
+            }
+            catch (Exception ex)
+            {
+                exception = ex;
+            };
+
+            var actual = exception.ShouldNotBeNull().ShouldBeOfType();
+            actual.ActualCount.ShouldBe(collection.Length);
+            actual.ExpectedCount.ShouldBe(1);
+        }
+
+        [Fact(DisplayName = "ShouldAllBe for Action throws CollectionException if one of " +
+                            "the element inspectors throws")]
+        public void Test004()
+        {
+            Exception? exception = null;
+
+            var collection = new string[] { "foo", "bar" };
+            try
+            {
+                collection.ShouldAllBe((x, i) => { }, (x, i) => throw new Exception());
+            }
+            catch (Exception ex)
+            {
+                exception = ex;
+            };
+
+            var actual = exception.ShouldNotBeNull().ShouldBeOfType();
+            actual.IndexFailurePoint.ShouldBe(1);
+        }
+
+        [Fact(DisplayName = "ShouldAllBe for Action passes elements to " +
+                            "the element inspectors in the order of collection")]
+        public void Test005()
+        {
+            var collection = new string[] { "foo", "bar" };
+
+            collection.ShouldAllBe(
+                x => x.ShouldBe(collection[0]),
+                x => x.ShouldBe(collection[1])
+            );
+        }
+
+        [Fact(DisplayName = "ShouldAllBe for Action passes elements to " +
+                    "the element inspectors in the order of collection, " +
+                   "with the matching index")]
+        public void Test006()
+        {
+            var collection = new string[] { "foo", "bar" };
+
+            collection.ShouldAllBe(
+                (x, i) => { x.ShouldBe(collection[0]); i.ShouldBe(0); },
+                (x, i) => { x.ShouldBe(collection[1]); i.ShouldBe(1); }
+            );
+        }
+
+    }
+}

From 49256287bfd9b495664d07b4188d96aab2978726 Mon Sep 17 00:00:00 2001
From: Egil Hansen 
Date: Thu, 16 Jan 2020 12:50:38 +0000
Subject: [PATCH 06/30] Added tests for general events and touch events
 dispatch extensions

---
 .../GeneralEventDispatchExtensions.cs         |  2 -
 .../TouchEventDispatchExtensions.cs           | 36 ++++++++++++------
 .../GeneralEventDispatchExtensionsTest.cs     | 37 +++++++++++++++++++
 3 files changed, 61 insertions(+), 14 deletions(-)

diff --git a/src/EventDispatchExtensions/GeneralEventDispatchExtensions.cs b/src/EventDispatchExtensions/GeneralEventDispatchExtensions.cs
index 2e6cef37d..32e5ddd40 100644
--- a/src/EventDispatchExtensions/GeneralEventDispatchExtensions.cs
+++ b/src/EventDispatchExtensions/GeneralEventDispatchExtensions.cs
@@ -10,8 +10,6 @@
 
 namespace Egil.RazorComponents.Testing.EventDispatchExtensions
 {
-    // TODO: add support for all event types listed here: https://github.com/aspnet/AspNetCore/blob/master/src/Components/Web/src/Web/EventHandlers.cs
-
     /// 
     /// General event dispatch helper extension methods.
     /// 
diff --git a/src/EventDispatchExtensions/TouchEventDispatchExtensions.cs b/src/EventDispatchExtensions/TouchEventDispatchExtensions.cs
index cbafe58c9..65bbd1f9a 100644
--- a/src/EventDispatchExtensions/TouchEventDispatchExtensions.cs
+++ b/src/EventDispatchExtensions/TouchEventDispatchExtensions.cs
@@ -42,7 +42,8 @@ public static void TouchCancel(this IElement element, long detail = default, Tou
         /// 
         /// The element to raise the event on.
         /// The event arguments to pass to the event handler.
-        public static void TouchCancel(this IElement element, TouchEventArgs eventArgs) => _ = TouchCancelAsync(element, eventArgs);
+        public static void TouchCancel(this IElement element, TouchEventArgs eventArgs) 
+            => _ = TouchCancelAsync(element, eventArgs);
 
         /// 
         /// Raises the @ontouchcancel event on , passing the provided 
@@ -51,7 +52,8 @@ public static void TouchCancel(this IElement element, long detail = default, Tou
         /// 
         /// 
         /// A task that completes when the event handler is done.
-        public static Task TouchCancelAsync(this IElement element, TouchEventArgs eventArgs) => element.TriggerEventAsync("ontouchcancel", eventArgs);
+        public static Task TouchCancelAsync(this IElement element, TouchEventArgs eventArgs) 
+            => element.TriggerEventAsync("ontouchcancel", eventArgs);
 
         /// 
         /// Raises the @ontouchend event on , passing the provided
@@ -82,7 +84,8 @@ public static void TouchEnd(this IElement element, long detail = default, TouchP
         /// 
         /// The element to raise the event on.
         /// The event arguments to pass to the event handler.
-        public static void TouchEnd(this IElement element, TouchEventArgs eventArgs) => _ = TouchEndAsync(element, eventArgs);
+        public static void TouchEnd(this IElement element, TouchEventArgs eventArgs) 
+            => _ = TouchEndAsync(element, eventArgs);
 
         /// 
         /// Raises the @ontouchend event on , passing the provided 
@@ -91,7 +94,8 @@ public static void TouchEnd(this IElement element, long detail = default, TouchP
         /// 
         /// 
         /// A task that completes when the event handler is done.
-        public static Task TouchEndAsync(this IElement element, TouchEventArgs eventArgs) => element.TriggerEventAsync("ontouchend", eventArgs);
+        public static Task TouchEndAsync(this IElement element, TouchEventArgs eventArgs) 
+            => element.TriggerEventAsync("ontouchend", eventArgs);
 
         /// 
         /// Raises the @ontouchmove event on , passing the provided
@@ -122,7 +126,8 @@ public static void TouchMove(this IElement element, long detail = default, Touch
         /// 
         /// The element to raise the event on.
         /// The event arguments to pass to the event handler.
-        public static void TouchMove(this IElement element, TouchEventArgs eventArgs) => _ = TouchMoveAsync(element, eventArgs);
+        public static void TouchMove(this IElement element, TouchEventArgs eventArgs) 
+            => _ = TouchMoveAsync(element, eventArgs);
 
         /// 
         /// Raises the @ontouchmove event on , passing the provided 
@@ -131,7 +136,8 @@ public static void TouchMove(this IElement element, long detail = default, Touch
         /// 
         /// 
         /// A task that completes when the event handler is done.
-        public static Task TouchMoveAsync(this IElement element, TouchEventArgs eventArgs) => element.TriggerEventAsync("ontouchmove", eventArgs);
+        public static Task TouchMoveAsync(this IElement element, TouchEventArgs eventArgs) 
+            => element.TriggerEventAsync("ontouchmove", eventArgs);
 
         /// 
         /// Raises the @ontouchstart event on , passing the provided
@@ -162,7 +168,8 @@ public static void TouchStart(this IElement element, long detail = default, Touc
         /// 
         /// The element to raise the event on.
         /// The event arguments to pass to the event handler.
-        public static void TouchStart(this IElement element, TouchEventArgs eventArgs) => _ = TouchStartAsync(element, eventArgs);
+        public static void TouchStart(this IElement element, TouchEventArgs eventArgs) 
+            => _ = TouchStartAsync(element, eventArgs);
 
         /// 
         /// Raises the @ontouchstart event on , passing the provided 
@@ -171,7 +178,8 @@ public static void TouchStart(this IElement element, long detail = default, Touc
         /// 
         /// 
         /// A task that completes when the event handler is done.
-        public static Task TouchStartAsync(this IElement element, TouchEventArgs eventArgs) => element.TriggerEventAsync("ontouchstart", eventArgs);
+        public static Task TouchStartAsync(this IElement element, TouchEventArgs eventArgs) 
+            => element.TriggerEventAsync("ontouchstart", eventArgs);
 
         /// 
         /// Raises the @ontouchenter event on , passing the provided
@@ -202,7 +210,8 @@ public static void TouchEnter(this IElement element, long detail = default, Touc
         /// 
         /// The element to raise the event on.
         /// The event arguments to pass to the event handler.
-        public static void TouchEnter(this IElement element, TouchEventArgs eventArgs) => _ = TouchEnterAsync(element, eventArgs);
+        public static void TouchEnter(this IElement element, TouchEventArgs eventArgs) 
+            => _ = TouchEnterAsync(element, eventArgs);
 
         /// 
         /// Raises the @ontouchenter event on , passing the provided 
@@ -211,7 +220,8 @@ public static void TouchEnter(this IElement element, long detail = default, Touc
         /// 
         /// 
         /// A task that completes when the event handler is done.
-        public static Task TouchEnterAsync(this IElement element, TouchEventArgs eventArgs) => element.TriggerEventAsync("ontouchenter", eventArgs);
+        public static Task TouchEnterAsync(this IElement element, TouchEventArgs eventArgs) 
+            => element.TriggerEventAsync("ontouchenter", eventArgs);
 
         /// 
         /// Raises the @ontouchleave event on , passing the provided
@@ -242,7 +252,8 @@ public static void TouchLeave(this IElement element, long detail = default, Touc
         /// 
         /// The element to raise the event on.
         /// The event arguments to pass to the event handler.
-        public static void TouchLeave(this IElement element, TouchEventArgs eventArgs) => _ = TouchLeaveAsync(element, eventArgs);
+        public static void TouchLeave(this IElement element, TouchEventArgs eventArgs) 
+            => _ = TouchLeaveAsync(element, eventArgs);
 
         /// 
         /// Raises the @ontouchleave event on , passing the provided 
@@ -251,6 +262,7 @@ public static void TouchLeave(this IElement element, long detail = default, Touc
         /// 
         /// 
         /// A task that completes when the event handler is done.
-        public static Task TouchLeaveAsync(this IElement element, TouchEventArgs eventArgs) => element.TriggerEventAsync("ontouchleave", eventArgs);
+        public static Task TouchLeaveAsync(this IElement element, TouchEventArgs eventArgs) 
+            => element.TriggerEventAsync("ontouchleave", eventArgs);
     }
 }
diff --git a/tests/EventDispatchExtensions/GeneralEventDispatchExtensionsTest.cs b/tests/EventDispatchExtensions/GeneralEventDispatchExtensionsTest.cs
index 45fc69c9c..87b715b34 100644
--- a/tests/EventDispatchExtensions/GeneralEventDispatchExtensionsTest.cs
+++ b/tests/EventDispatchExtensions/GeneralEventDispatchExtensionsTest.cs
@@ -4,6 +4,10 @@
 using System.Reflection;
 using System.Text;
 using System.Threading.Tasks;
+using AngleSharp;
+using AngleSharp.Dom;
+using Moq;
+using Shouldly;
 using Xunit;
 
 namespace Egil.RazorComponents.Testing.EventDispatchExtensions
@@ -22,5 +26,38 @@ public async Task CanRaiseEvents(MethodInfo helper)
 
             await VerifyEventRaisesCorrectly(helper, EventArgs.Empty);
         }
+
+        [Fact(DisplayName = "TriggerEventAsync throws element is null")]
+        public void Test001()
+        {
+            IElement elm = default!;
+            Should.Throw(() => elm.TriggerEventAsync("", EventArgs.Empty))
+                .ParamName.ShouldBe("element");
+        }
+
+        [Fact(DisplayName = "TriggerEventAsync throws if element does not contain an attribute with the blazor event-name")]
+        public void Test002()
+        {
+            var elmMock = new Mock();
+            elmMock.Setup(x => x.GetAttribute(It.IsAny())).Returns(() => null!);
+
+            Should.Throw(() => elmMock.Object.TriggerEventAsync("click", EventArgs.Empty));
+        }
+
+        [Fact(DisplayName = "TriggerEventAsync throws if element was not rendered through blazor (has a TestRendere in its context)")]
+        public void Test003()
+        {
+            var elmMock = new Mock();
+            var docMock = new Mock();
+            var ctxMock = new Mock();
+
+            elmMock.Setup(x => x.GetAttribute(It.IsAny())).Returns("1");
+            elmMock.SetupGet(x => x.Owner).Returns(docMock.Object);
+            docMock.SetupGet(x => x.Context).Returns(ctxMock.Object);
+            ctxMock.Setup(x => x.GetService()).Returns(() => null!);
+
+            Should.Throw(() => elmMock.Object.TriggerEventAsync("click", EventArgs.Empty));
+        }
+
     }
 }

From 26be4679bdc978ad4e7782595659412c37794977 Mon Sep 17 00:00:00 2001
From: Egil Hansen 
Date: Thu, 16 Jan 2020 12:51:13 +0000
Subject: [PATCH 07/30] Added tests for anglesharp extensions,
 JsRuntimeInvocation and ComponentParameter

---
 sample/tests/Assembly.cs                      |  1 +
 src/Extensions/AngleSharpExtensions.cs        |  1 -
 src/Mocking/JSInterop/JsRuntimeInvocation.cs  | 35 +++++++--
 src/Rendering/ComponentParameter.cs           | 29 ++++----
 ...Components.Testing.Library.Template.csproj |  4 ++
 tests/Assembly.cs                             |  1 +
 ...zorComponents.Testing.Library.Tests.csproj | 11 +++
 tests/GlobalSuppressions.cs                   |  1 +
 .../JSInterop/JsRuntimeInvocationTest.cs      | 59 +++++++++++++++
 .../MockJsRuntimeInvokeHandlerTest.cs         |  0
 tests/Mocking/MockHttpExtensionsTest.cs       | 56 +++++++++++++++
 tests/Rendering/ComponentParameterTest.cs     | 72 +++++++++++++++++++
 tests/TestUtililities/MockingHelpers.cs       | 67 +++++++++++++++++
 13 files changed, 317 insertions(+), 20 deletions(-)
 create mode 100644 sample/tests/Assembly.cs
 create mode 100644 tests/Assembly.cs
 create mode 100644 tests/Mocking/JSInterop/JsRuntimeInvocationTest.cs
 rename tests/{ => Mocking}/JSInterop/MockJsRuntimeInvokeHandlerTest.cs (100%)
 create mode 100644 tests/Mocking/MockHttpExtensionsTest.cs
 create mode 100644 tests/Rendering/ComponentParameterTest.cs
 create mode 100644 tests/TestUtililities/MockingHelpers.cs

diff --git a/sample/tests/Assembly.cs b/sample/tests/Assembly.cs
new file mode 100644
index 000000000..c2a9bc9c5
--- /dev/null
+++ b/sample/tests/Assembly.cs
@@ -0,0 +1 @@
+[assembly: System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
\ No newline at end of file
diff --git a/src/Extensions/AngleSharpExtensions.cs b/src/Extensions/AngleSharpExtensions.cs
index 3eda41cf6..58560fc3d 100644
--- a/src/Extensions/AngleSharpExtensions.cs
+++ b/src/Extensions/AngleSharpExtensions.cs
@@ -194,7 +194,6 @@ public static IEnumerable AsEnumerable(this INode node)
             yield return node;
         }
 
-
         /// 
         /// Gets the  stored in the s
         /// owning context, if one is available. 
diff --git a/src/Mocking/JSInterop/JsRuntimeInvocation.cs b/src/Mocking/JSInterop/JsRuntimeInvocation.cs
index a12d0095a..a6437e647 100644
--- a/src/Mocking/JSInterop/JsRuntimeInvocation.cs
+++ b/src/Mocking/JSInterop/JsRuntimeInvocation.cs
@@ -27,26 +27,53 @@ namespace Egil.RazorComponents.Testing
         /// 
         /// Creates an instance of the .
         /// 
-        public JsRuntimeInvocation(string identifier, CancellationToken? cancellationToken, object[] args)
+        public JsRuntimeInvocation(string identifier, CancellationToken cancellationToken, object[] args)
         {
             Identifier = identifier;
-            CancellationToken = cancellationToken ?? CancellationToken.None;
+            CancellationToken = cancellationToken;
             Arguments = args;
         }
 
         /// 
-        public bool Equals(JsRuntimeInvocation other) => Identifier.Equals(other.Identifier, StringComparison.Ordinal) && CancellationToken == other.CancellationToken && Arguments == other.Arguments;
+        public bool Equals(JsRuntimeInvocation other)
+            => Identifier.Equals(other.Identifier, StringComparison.Ordinal)
+            && CancellationToken == other.CancellationToken
+            && ArgumentsEqual(Arguments, other.Arguments);
 
         /// 
         public override bool Equals(object obj) => obj is JsRuntimeInvocation other && Equals(other);
 
         /// 
-        public override int GetHashCode() => (Identifier, CancellationToken, Arguments).GetHashCode();
+        public override int GetHashCode()
+        {
+            var hash = new HashCode();
+            hash.Add(Identifier);
+            hash.Add(CancellationToken);
+
+            for (int i = 0; i < Arguments.Count; i++)
+            {
+                hash.Add(Arguments[i]);
+            }
+
+            return hash.ToHashCode();
+        }
 
         /// 
         public static bool operator ==(JsRuntimeInvocation left, JsRuntimeInvocation right) => left.Equals(right);
 
         /// 
         public static bool operator !=(JsRuntimeInvocation left, JsRuntimeInvocation right) => !(left == right);
+
+        private static bool ArgumentsEqual(IReadOnlyList left, IReadOnlyList right)
+        {
+            if (left.Count != right.Count) return false;
+
+            for (int i = 0; i < left.Count; i++)
+            {
+                if (!left[i].Equals(right[i])) return false;
+            }
+
+            return true;
+        }
     }
 }
diff --git a/src/Rendering/ComponentParameter.cs b/src/Rendering/ComponentParameter.cs
index e652f587c..b1ba4a302 100644
--- a/src/Rendering/ComponentParameter.cs
+++ b/src/Rendering/ComponentParameter.cs
@@ -34,7 +34,7 @@ private ComponentParameter(string? name, object? value, bool isCascadingValue)
             if (isCascadingValue && value is null)
                 throw new ArgumentNullException(nameof(value), "Cascading values cannot be set to null");
 
-            if(!isCascadingValue && name is null)
+            if (!isCascadingValue && name is null)
                 throw new ArgumentNullException(nameof(name), "A parameters name cannot be set to null");
 
             Name = name;
@@ -47,46 +47,45 @@ private ComponentParameter(string? name, object? value, bool isCascadingValue)
         /// 
         /// Name of the parameter to pass to the component
         /// Value or null to pass the component
-        public static ComponentParameter CreateParameter(string name, object? value) => new ComponentParameter(name, value, false);
+        public static ComponentParameter CreateParameter(string name, object? value) 
+            => new ComponentParameter(name, value, false);
 
         /// 
         /// Create a Cascading Value parameter for a component under test.
         /// 
         /// A optional name for the cascading value
         /// The cascading value
-        public static ComponentParameter CreateCascadingValue(string? name, object value) => new ComponentParameter(name, value, true);
+        public static ComponentParameter CreateCascadingValue(string? name, object value) 
+            => new ComponentParameter(name, value, true);
 
         /// 
         /// Create a parameter for a component under test.
         /// 
         /// A name/value pair for the parameter
-        public static implicit operator ComponentParameter((string name, object? value) input) => CreateParameter(input.name, input.value);
+        public static implicit operator ComponentParameter((string name, object? value) input) 
+            => CreateParameter(input.name, input.value);
 
         /// 
         /// Create a parameter or cascading value for a component under test.
         /// 
         /// A name/value/isCascadingValue triple for the parameter
-        public static implicit operator ComponentParameter((string? name, object? value, bool isCascadingValue) input) => new ComponentParameter(input.name, input.value, input.isCascadingValue);
+        public static implicit operator ComponentParameter((string? name, object? value, bool isCascadingValue) input) 
+            => new ComponentParameter(input.name, input.value, input.isCascadingValue);
 
         /// 
-        public bool Equals(ComponentParameter other) => Name == other.Name && Value == other.Value && IsCascadingValue == other.IsCascadingValue;
+        public bool Equals(ComponentParameter other)
+            => string.Equals(Name, other.Name, StringComparison.Ordinal) && Value == other.Value && IsCascadingValue == other.IsCascadingValue;
 
         /// 
         public override bool Equals(object obj) => obj is ComponentParameter other && Equals(other);
 
         /// 
-        public override int GetHashCode() => (Name, Value, IsCascadingValue).GetHashCode();
+        public override int GetHashCode() => HashCode.Combine(Name, Value, IsCascadingValue);
 
         /// 
-        public static bool operator ==(ComponentParameter left, ComponentParameter right)
-        {
-            return left.Equals(right);
-        }
+        public static bool operator ==(ComponentParameter left, ComponentParameter right) => left.Equals(right);
 
         /// 
-        public static bool operator !=(ComponentParameter left, ComponentParameter right)
-        {
-            return !(left == right);
-        }
+        public static bool operator !=(ComponentParameter left, ComponentParameter right) => !(left == right);
     }
 }
diff --git a/template/Razor.Components.Testing.Library.Template.csproj b/template/Razor.Components.Testing.Library.Template.csproj
index 4c6922876..64e582ac4 100644
--- a/template/Razor.Components.Testing.Library.Template.csproj
+++ b/template/Razor.Components.Testing.Library.Template.csproj
@@ -26,6 +26,10 @@ This library's goal is to make it easy to write comprehensive, stable unit tests
   
     
     
+    
+    
+    
+    
   
 
 
\ No newline at end of file
diff --git a/tests/Assembly.cs b/tests/Assembly.cs
new file mode 100644
index 000000000..c2a9bc9c5
--- /dev/null
+++ b/tests/Assembly.cs
@@ -0,0 +1 @@
+[assembly: System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
\ No newline at end of file
diff --git a/tests/Egil.RazorComponents.Testing.Library.Tests.csproj b/tests/Egil.RazorComponents.Testing.Library.Tests.csproj
index 304d81e50..45fa173bd 100644
--- a/tests/Egil.RazorComponents.Testing.Library.Tests.csproj
+++ b/tests/Egil.RazorComponents.Testing.Library.Tests.csproj
@@ -7,6 +7,17 @@
   
 
   
+    
+    
+    
+    
+  
+
+  
+    
+      runtime; build; native; contentfiles; analyzers; buildtransitive
+      all
+    
     
     
     
diff --git a/tests/GlobalSuppressions.cs b/tests/GlobalSuppressions.cs
index 241c968a9..6e6c9ad0b 100644
--- a/tests/GlobalSuppressions.cs
+++ b/tests/GlobalSuppressions.cs
@@ -3,3 +3,4 @@
 [assembly: SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "")]
 [assembly: SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "")]
 [assembly: SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "")]
+[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "In tests its ok to catch the general exception type")]
diff --git a/tests/Mocking/JSInterop/JsRuntimeInvocationTest.cs b/tests/Mocking/JSInterop/JsRuntimeInvocationTest.cs
new file mode 100644
index 000000000..41c373a1a
--- /dev/null
+++ b/tests/Mocking/JSInterop/JsRuntimeInvocationTest.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Shouldly;
+using Xunit;
+
+namespace Egil.RazorComponents.Testing.Mocking.JSInterop
+{
+    public class JsRuntimeInvocationTest
+    {
+        public static IEnumerable GetEqualsTestData()
+        {
+            var token = new CancellationToken(true);
+            var args = new object[] { 1, "baz" };
+
+            var i1 = new JsRuntimeInvocation("foo", token, args);
+            var i2 = new JsRuntimeInvocation("foo", token, args);
+            var i3 = new JsRuntimeInvocation("bar", token, args);
+            var i4 = new JsRuntimeInvocation("foo", CancellationToken.None, args);
+            var i5 = new JsRuntimeInvocation("foo", token, Array.Empty());
+            var i6 = new JsRuntimeInvocation("foo", token, new object[] { 2, "woop" });
+
+            yield return new object[] { i1, i1, true };
+            yield return new object[] { i1, i2, true };
+            yield return new object[] { i1, i3, false };
+            yield return new object[] { i1, i4, false };
+            yield return new object[] { i1, i5, false };
+            yield return new object[] { i1, i6, false };
+        }
+
+        [Theory(DisplayName = "Equals operator works as expected")]
+        [MemberData(nameof(GetEqualsTestData))]
+        public void Test002(JsRuntimeInvocation left, JsRuntimeInvocation right, bool expectedResult)
+        {
+            left.Equals(right).ShouldBe(expectedResult);
+            right.Equals(left).ShouldBe(expectedResult);
+            (left == right).ShouldBe(expectedResult);
+            (left != right).ShouldNotBe(expectedResult);
+            left.Equals((object)right).ShouldBe(expectedResult);
+            right.Equals((object)left).ShouldBe(expectedResult);
+        }
+
+        [Fact(DisplayName = "Equals operator works as expected with non compatible types")]
+        public void Test003()
+        {
+            new JsRuntimeInvocation().Equals(new object()).ShouldBeFalse();
+        }
+
+        [Theory(DisplayName = "GetHashCode returns same result for equal JsRuntimeInvocations")]
+        [MemberData(nameof(GetEqualsTestData))]
+        public void Test004(JsRuntimeInvocation left, JsRuntimeInvocation right, bool expectedResult)
+        {
+            left.GetHashCode().Equals(right.GetHashCode()).ShouldBe(expectedResult);
+        }
+    }
+}
diff --git a/tests/JSInterop/MockJsRuntimeInvokeHandlerTest.cs b/tests/Mocking/JSInterop/MockJsRuntimeInvokeHandlerTest.cs
similarity index 100%
rename from tests/JSInterop/MockJsRuntimeInvokeHandlerTest.cs
rename to tests/Mocking/JSInterop/MockJsRuntimeInvokeHandlerTest.cs
diff --git a/tests/Mocking/MockHttpExtensionsTest.cs b/tests/Mocking/MockHttpExtensionsTest.cs
new file mode 100644
index 000000000..9aac81fdf
--- /dev/null
+++ b/tests/Mocking/MockHttpExtensionsTest.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using RichardSzalay.MockHttp;
+using Shouldly;
+using Xunit;
+
+namespace Egil.RazorComponents.Testing.Mocking
+{
+    public class MockHttpExtensionsTest
+    {
+        [Fact(DisplayName = "AddMockHttp throws if the service provider is null")]
+        public void Test001()
+        {
+            TestServiceProvider provider = default!;
+
+            Should.Throw(() => provider.AddMockHttp());
+        }
+
+        [Fact(DisplayName = "AddMockHttp registers a mock HttpClient in the service provider")]
+        public void Test002()
+        {
+            using var provider = new TestServiceProvider();
+
+            var mock = provider.AddMockHttp();
+
+            provider.GetService().ShouldNotBeNull();
+        }
+
+        [Fact(DisplayName = "Capture  throws if the handler is null")]
+        public void Test003()
+        {
+            MockHttpMessageHandler handler = default!;
+
+            Should.Throw(() => handler.Capture(""));
+        }
+
+        [Fact(DisplayName = "Capture returns a task, that when completed, " +
+                            "provides a response to the captured url")]
+        public async Task Test004()
+        {
+            using var provider = new TestServiceProvider();
+            var mock = provider.AddMockHttp();
+            var httpClient = provider.GetService();
+            var captured = mock.Capture("/ping");
+
+            captured.SetResult("pong");
+
+            var actual = await httpClient.GetStringAsync("/ping");
+            actual.ShouldBe("\"pong\"");
+        }
+    }
+}
diff --git a/tests/Rendering/ComponentParameterTest.cs b/tests/Rendering/ComponentParameterTest.cs
new file mode 100644
index 000000000..291824693
--- /dev/null
+++ b/tests/Rendering/ComponentParameterTest.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Shouldly;
+using Xunit;
+
+namespace Egil.RazorComponents.Testing.Rendering
+{
+    public class ComponentParameterTest
+    {
+        public static IEnumerable GetEqualsTestData()
+        {
+            var name = "foo";
+            var value = "bar";
+            var p1 = ComponentParameter.CreateParameter(name, value);
+            var p2 = ComponentParameter.CreateParameter(name, value);
+            var p3 = ComponentParameter.CreateCascadingValue(name, value);
+            var p4 = ComponentParameter.CreateParameter(string.Empty, value);
+            var p5 = ComponentParameter.CreateParameter(name, string.Empty);
+
+            yield return new object[] { p1, p1, true };
+            yield return new object[] { p1, p2, true };
+            yield return new object[] { p3, p3, true };
+            yield return new object[] { p1, p3, false };
+            yield return new object[] { p1, p4, false };
+            yield return new object[] { p1, p5, false };
+        }
+
+        [Fact(DisplayName = "Creating a cascading value throws")]
+        public void Test001()
+        {
+            Should.Throw(() => ComponentParameter.CreateCascadingValue(null, null!));
+            Should.Throw(() => { ComponentParameter p = (null, null, true); });
+        }
+
+        [Fact(DisplayName = "Creating a regular parameter without a name throws")]
+        public void Test002()
+        {
+            Should.Throw(() => ComponentParameter.CreateParameter(null!, null));
+            Should.Throw(() => { ComponentParameter p = (null, null, false); });
+        }
+
+        [Theory(DisplayName = "Equals compares correctly")]
+        [MemberData(nameof(GetEqualsTestData))]
+        public void Test003(ComponentParameter left, ComponentParameter right, bool expectedResult)
+        {
+            left.Equals(right).ShouldBe(expectedResult);
+            right.Equals(left).ShouldBe(expectedResult);
+            (left == right).ShouldBe(expectedResult);
+            (left != right).ShouldNotBe(expectedResult);
+            left.Equals((object)right).ShouldBe(expectedResult);
+            right.Equals((object)left).ShouldBe(expectedResult);
+        }
+
+        [Fact(DisplayName = "Equals operator works as expected with non compatible types")]
+        public void Test004()
+        {
+            ComponentParameter.CreateParameter(string.Empty, string.Empty)
+                .Equals(new object())
+                .ShouldBeFalse();
+        }
+
+        [Theory(DisplayName = "GetHashCode returns same result for equal ComponentParameter")]
+        [MemberData(nameof(GetEqualsTestData))]
+        public void Test005(ComponentParameter left, ComponentParameter right, bool expectedResult)
+        {
+            left.GetHashCode().Equals(right.GetHashCode()).ShouldBe(expectedResult);
+        }
+    }
+}
diff --git a/tests/TestUtililities/MockingHelpers.cs b/tests/TestUtililities/MockingHelpers.cs
new file mode 100644
index 000000000..c3154ada8
--- /dev/null
+++ b/tests/TestUtililities/MockingHelpers.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Linq;
+using System.Reflection;
+using Moq;
+
+namespace Egil.RazorComponents.Testing.TestUtililities
+{
+    /// 
+    /// Helper methods for creating mocks.
+    /// 
+    public static class MockingHelpers
+    {
+        private static readonly MethodInfo MockOfInfo = typeof(Mock)
+            .GetMethods()
+            .Where(x => x.Name == nameof(Mock.Of))
+            .First(x => x.GetParameters().Length == 0);
+
+        private static readonly Type DelegateType = typeof(MulticastDelegate);
+        private static readonly Type StringType = typeof(string);
+
+        /// 
+        /// Creates a mock instance of .
+        /// 
+        /// Type to create a mock of.
+        /// An instance of .
+        public static object ToMockInstance(this Type type)
+        {
+            // Type to mock must be an interface, a delegate, or a non-sealed, non-static class.
+            if (type.IsMockable())
+            {
+                var result = MockOfInfo.MakeGenericMethod(type).Invoke(null, Array.Empty());
+
+                if (result is null)
+                    throw new NotSupportedException($"Cannot create an mock of {type.FullName}.");
+
+                return result;
+            }
+            else if (type.Equals(StringType))
+            {
+                return string.Empty;
+            }
+            else
+            {
+                throw new NotSupportedException($"Cannot create an mock of {type.FullName}. Type to mock must be an interface, a delegate, or a non-sealed, non-static class.");
+            }
+        }
+
+        /// 
+        /// Gets whether a type is mockable by .
+        /// 
+        public static bool IsMockable(this Type type)
+        {
+            if (type.IsSealed)
+                return type.IsDelegateType();
+            return true;
+        }
+
+        /// 
+        /// Gets whether a type is a delegate type.
+        /// 
+        public static bool IsDelegateType(this Type type)
+        {
+            return Equals(type, DelegateType);
+        }
+
+    }
+}

From e99b1d765091fbe9c46fa23110ea9ed73d201445 Mon Sep 17 00:00:00 2001
From: Egil Hansen 
Date: Thu, 16 Jan 2020 14:51:15 +0000
Subject: [PATCH 08/30] Removed assert helpers that conflict with Shoudly

---
 sample/tests/Tests/Components/TodoListTest.cs |  4 +-
 src/Asserting/CollectionAssertExtensions.cs   | 51 -----------
 src/Asserting/DiffAssertExtensions.cs         |  2 +-
 tests/Asserting/DiffAssertExtensionsTest.cs   | 76 ++++++++++++++++
 tests/Asserting/GenericAssertExtensions.cs    | 87 -------------------
 .../GenericCollectionAssertExtensionsTest.cs  |  8 +-
 6 files changed, 83 insertions(+), 145 deletions(-)
 create mode 100644 tests/Asserting/DiffAssertExtensionsTest.cs
 delete mode 100644 tests/Asserting/GenericAssertExtensions.cs

diff --git a/sample/tests/Tests/Components/TodoListTest.cs b/sample/tests/Tests/Components/TodoListTest.cs
index e7478c734..17bc30280 100644
--- a/sample/tests/Tests/Components/TodoListTest.cs
+++ b/sample/tests/Tests/Components/TodoListTest.cs
@@ -119,8 +119,8 @@ public void Test005()
             cut.Find("input").Change(taskValue);
             cut.Find("form").Submit();
 
-            createdTask.ShouldNotBeNull();
-            createdTask?.Text.ShouldBe(taskValue);
+            createdTask = createdTask.ShouldBeOfType();
+            createdTask.Text.ShouldBe(taskValue);
         }
 
         [Fact(DisplayName = "When add task form is submitted with no text OnAddingTodo is not called")]
diff --git a/src/Asserting/CollectionAssertExtensions.cs b/src/Asserting/CollectionAssertExtensions.cs
index 3d5198d10..5d3609797 100644
--- a/src/Asserting/CollectionAssertExtensions.cs
+++ b/src/Asserting/CollectionAssertExtensions.cs
@@ -7,62 +7,11 @@
 
 namespace Egil.RazorComponents.Testing.Asserting
 {
-    /// 
-    /// Collection test assertions
-    /// 
-    public static class GenericAssertExtensions
-    {
-        /// 
-        /// Verifies that  is not null
-        /// and returns  again.
-        /// 
-        /// Returns  if it is not null.
-        public static T ShouldNotBeNull([NotNullIfNotNull("actual")]this T? actual) where T : class
-        {
-            if (actual is null)
-                throw new XunitException($"{nameof(ShouldNotBeNull)}() Failure");
-            return actual;
-        }
-
-        /// 
-        /// Verifies that  is not null
-        /// and returns  again.
-        /// 
-        /// Returns  if it is not null.
-        public static T ShouldNotBeNull([NotNullIfNotNull("actual")]this T? actual) where T : struct
-        {
-            if (actual is null)
-                throw new XunitException($"{nameof(ShouldNotBeNull)}() Failure");
-            return actual.Value;
-        }
-
-        /// 
-        /// Verifies that a nullable  is not null
-        /// and of type .
-        /// 
-        /// Returns  as .
-        public static T ShouldBeOfType([NotNullIfNotNull("actual")]this object? actual)
-        {
-            return Assert.IsType(actual);
-        }
-
-        /// 
-        /// Verifies that a non nullable struct is the same as its nullable counter part.
-        /// 
-        public static void ShouldBe(this T actual, T? expected)
-             where T : struct
-        {
-            Assert.Equal(expected, actual);
-        }
-    }
-
     /// 
     /// Collection test assertions
     /// 
     public static class CollectionAssertExtensions
     {
-
-
         /// 
         /// Verifies that a collection contains exactly a given number of elements, which
         /// meet the criteria provided by the element inspectors.
diff --git a/src/Asserting/DiffAssertExtensions.cs b/src/Asserting/DiffAssertExtensions.cs
index 461194674..1b1d2d5c3 100644
--- a/src/Asserting/DiffAssertExtensions.cs
+++ b/src/Asserting/DiffAssertExtensions.cs
@@ -37,7 +37,7 @@ public static IDiff ShouldHaveSingleChange(this IReadOnlyList diffs)
         /// The total number of  inspectors must exactly match the number of s in the collection
         public static void ShouldHaveChanges(this IReadOnlyList diffs, params Action[] diffInspectors)
         {
-            Assert.Collection(diffs, diffInspectors);
+            CollectionAssertExtensions.ShouldAllBe(diffs, diffInspectors);
         }
 
     }
diff --git a/tests/Asserting/DiffAssertExtensionsTest.cs b/tests/Asserting/DiffAssertExtensionsTest.cs
new file mode 100644
index 000000000..e974ac857
--- /dev/null
+++ b/tests/Asserting/DiffAssertExtensionsTest.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using AngleSharp.Diffing.Core;
+using Egil.RazorComponents.Testing.Diffing;
+using Moq;
+using Shouldly;
+using Xunit;
+using Xunit.Sdk;
+
+namespace Egil.RazorComponents.Testing.Asserting
+{
+    public class DiffAssertExtensionsTest
+    {
+        [Fact(DisplayName = "ShouldHaveSingleChange throws when input is null")]
+        public void Test001()
+        {
+            IReadOnlyList? diffs = null;
+            Exception? exception = null;
+
+            try
+            {
+                DiffAssertExtensions.ShouldHaveSingleChange(diffs!);
+            }
+            catch (Exception ex)
+            {
+                exception = ex;
+            };
+
+            exception.ShouldBeOfType();
+        }
+
+        [Theory(DisplayName = "ShouldHaveSingleChange throws when input length not exactly 1")]
+        [MemberData(nameof(GetDiffLists))]
+        public void Test002(IReadOnlyList diffs)
+        {
+            Exception? exception = null;
+
+            try
+            {
+                diffs.ShouldHaveSingleChange();
+            }
+            catch (Exception ex)
+            {
+                exception = ex;
+            };
+
+            exception.ShouldBeOfType();
+        }
+
+        [Fact(DisplayName = "ShouldHaveSingleChange returns the single diff in input when there is only one")]
+        public void Test003()
+        {
+            var input = new IDiff[] { Mock.Of() };
+
+            var output = input.ShouldHaveSingleChange();
+
+            output.ShouldBe(input[0]);
+        }
+
+        internal static IEnumerable GetDiffLists()
+        {
+            yield return new object[] { Array.Empty() };
+            yield return new object[]
+            {
+                new IDiff[]
+                {
+                    Mock.Of(),
+                    Mock.Of(),
+                }
+            };
+        }
+    }
+}
diff --git a/tests/Asserting/GenericAssertExtensions.cs b/tests/Asserting/GenericAssertExtensions.cs
deleted file mode 100644
index 1a28aaabd..000000000
--- a/tests/Asserting/GenericAssertExtensions.cs
+++ /dev/null
@@ -1,87 +0,0 @@
-using System;
-using System.Collections.Generic;
-using Shouldly;
-using Xunit;
-using Xunit.Sdk;
-
-namespace Egil.RazorComponents.Testing.Asserting
-{
-    public class GenericAssertExtensions
-    {
-        [Fact(DisplayName = "ShouldNotBeNull throws exception when input is class and null")]
-        public void Test001()
-        {
-            Exception? exception = null;
-            object? input = null;
-            try
-            {
-                input.ShouldNotBeNull();
-            }
-            catch (Exception ex)
-            {
-                exception = ex;
-            }
-
-            var actual = exception.ShouldBeOfType();
-            actual.Message.ShouldContain("ShouldNotBeNull");
-        }
-
-        [Fact(DisplayName = "ShouldNotBeNull throws exception when input is struct and null")]
-        public void Test002()
-        {
-            Exception? exception = null;
-            int? input = null;
-            try
-            {
-                input.ShouldNotBeNull();
-            }
-            catch (Exception ex)
-            {
-                exception = ex;
-            }
-
-            var actual = exception.ShouldBeOfType();
-            actual.Message.ShouldContain("ShouldNotBeNull");
-        }
-
-        [Fact(DisplayName = "ShouldNotBeNull returns input is class and it is not null")]
-        public void Test003()
-        {
-            object? input = new object();
-            var output = input.ShouldNotBeNull();
-            output.ShouldBe(input);
-        }
-
-        [Fact(DisplayName = "ShouldNotBeNull returns input is struct and it is not null")]
-        public void Test004()
-        {
-            int? input = 42;
-            var output = input.ShouldNotBeNull();
-            output.ShouldBe(input);
-        }
-
-        [Fact(DisplayName = "ShouldBeOfType throws when actual is a different type")]
-        public void Test005()
-        {
-            Exception? exception = null;
-            try
-            {
-                "foo".ShouldBeOfType();
-            }
-            catch (Exception ex)
-            {
-                exception = ex;
-            }
-
-            Assert.IsType(exception);
-        }
-
-        [Fact(DisplayName = "ShouldBeOfType returns input when type is as expected")]
-        public void Test006()
-        {
-            object? input = "foo";
-            var actual = input.ShouldBeOfType();
-            actual.ShouldBe(input);
-        }
-    }
-}
diff --git a/tests/Asserting/GenericCollectionAssertExtensionsTest.cs b/tests/Asserting/GenericCollectionAssertExtensionsTest.cs
index 37e504d85..bdf6857bc 100644
--- a/tests/Asserting/GenericCollectionAssertExtensionsTest.cs
+++ b/tests/Asserting/GenericCollectionAssertExtensionsTest.cs
@@ -28,7 +28,7 @@ public void Test001()
                 exception = ex;
             };
 
-            var actual = exception.ShouldNotBeNull().ShouldBeOfType();
+            var actual = exception.ShouldBeOfType();
             actual.ActualCount.ShouldBe(collection.Length);
             actual.ExpectedCount.ShouldBe(1);
         }
@@ -49,7 +49,7 @@ public void Test002()
                 exception = ex;
             };
 
-            var actual = exception.ShouldNotBeNull().ShouldBeOfType();
+            var actual = exception.ShouldBeOfType();
             actual.IndexFailurePoint.ShouldBe(1);
         }
 
@@ -70,7 +70,7 @@ public void Test003()
                 exception = ex;
             };
 
-            var actual = exception.ShouldNotBeNull().ShouldBeOfType();
+            var actual = exception.ShouldBeOfType();
             actual.ActualCount.ShouldBe(collection.Length);
             actual.ExpectedCount.ShouldBe(1);
         }
@@ -91,7 +91,7 @@ public void Test004()
                 exception = ex;
             };
 
-            var actual = exception.ShouldNotBeNull().ShouldBeOfType();
+            var actual = exception.ShouldBeOfType();
             actual.IndexFailurePoint.ShouldBe(1);
         }
 

From 7dedd33d18968d4dae90973b5c8ac9d1f0a5a3b3 Mon Sep 17 00:00:00 2001
From: Egil Hansen 
Date: Thu, 16 Jan 2020 16:34:44 +0000
Subject: [PATCH 09/30] Tests of JsRuntimeAsserts

---
 src/Assembly.cs                               |   1 +
 .../JsInvokeCountExpectedException.cs         |  25 ++++
 .../JSInterop}/JsRuntimeAssertExtensions.cs   |  42 ++++--
 src/Mocking/MockJsRuntimeExtensions.cs        |   7 +
 src/Rendering/Htmlizer.cs                     |  10 +-
 .../JsRuntimeAssertExtensionsTest.cs          | 132 ++++++++++++++++++
 6 files changed, 204 insertions(+), 13 deletions(-)
 create mode 100644 src/Assembly.cs
 rename src/{Asserting => Mocking/JSInterop}/JsInvokeCountExpectedException.cs (54%)
 rename src/{Asserting => Mocking/JSInterop}/JsRuntimeAssertExtensions.cs (54%)
 create mode 100644 tests/Mocking/JSInterop/JsRuntimeAssertExtensionsTest.cs

diff --git a/src/Assembly.cs b/src/Assembly.cs
new file mode 100644
index 000000000..92a57b785
--- /dev/null
+++ b/src/Assembly.cs
@@ -0,0 +1 @@
+[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Egil.RazorComponents.Testing.Library.Tests")]
\ No newline at end of file
diff --git a/src/Asserting/JsInvokeCountExpectedException.cs b/src/Mocking/JSInterop/JsInvokeCountExpectedException.cs
similarity index 54%
rename from src/Asserting/JsInvokeCountExpectedException.cs
rename to src/Mocking/JSInterop/JsInvokeCountExpectedException.cs
index 636bf70ad..6c3468c4d 100644
--- a/src/Asserting/JsInvokeCountExpectedException.cs
+++ b/src/Mocking/JSInterop/JsInvokeCountExpectedException.cs
@@ -1,15 +1,40 @@
 using System;
 using System.Diagnostics.CodeAnalysis;
+using Egil.RazorComponents.Testing;
 using Xunit.Sdk;
 
 namespace Xunit.Sdk
 {
+    /// 
+    /// Represents a number of unexpected invocation to a .
+    /// 
     [SuppressMessage("Design", "CA1032:Implement standard exception constructors", Justification = "")]
     public class JsInvokeCountExpectedException : AssertActualExpectedException
     {
+        /// 
+        /// Gets the expected invocation count.
+        /// 
+        public int ExpectedInvocationCount { get; }
+
+        /// 
+        /// Gets the actual invocation count.
+        /// 
+        public int ActualInvocationCount { get; }
+
+        /// 
+        /// Gets the identifier.
+        /// 
+        public string Identifier { get; }
+
+        /// 
+        /// Creates an instance of the .
+        /// 
         public JsInvokeCountExpectedException(string identifier, int expectedCount, int actualCount, string assertMethod, string? userMessage = null)
             : base(expectedCount, actualCount, CreateMessage(assertMethod, identifier, userMessage), "Expected number of calls", "Actual number of calls")
         {
+            ExpectedInvocationCount = expectedCount;
+            ActualInvocationCount = actualCount;
+            Identifier = identifier;
         }
 
         private static string CreateMessage(string assertMethod, string identifier, string? userMessage = null)
diff --git a/src/Asserting/JsRuntimeAssertExtensions.cs b/src/Mocking/JSInterop/JsRuntimeAssertExtensions.cs
similarity index 54%
rename from src/Asserting/JsRuntimeAssertExtensions.cs
rename to src/Mocking/JSInterop/JsRuntimeAssertExtensions.cs
index 4fffda259..b2786e180 100644
--- a/src/Asserting/JsRuntimeAssertExtensions.cs
+++ b/src/Mocking/JSInterop/JsRuntimeAssertExtensions.cs
@@ -6,13 +6,19 @@
 using Xunit;
 using Xunit.Sdk;
 
-namespace Egil.RazorComponents.Testing.Asserting
+namespace Egil.RazorComponents.Testing
 {
     /// 
     /// Assert extensions for JsRuntimeMock
     /// 
     public static class JsRuntimeAssertExtensions
     {
+        /// 
+        /// Verifies that the  was never invoked on the .
+        /// 
+        /// Handler to verify against.
+        /// Identifier of invocation that should not have happened.
+        /// A custom user message to display if the assertion fails.
         public static void VerifyNotInvoke(this MockJsRuntimeInvokeHandler handler, string identifier, string? userMessage = null)
         {
             if (handler is null) throw new ArgumentNullException(nameof(handler));
@@ -22,9 +28,24 @@ public static void VerifyNotInvoke(this MockJsRuntimeInvokeHandler handler, stri
             }
         }
 
-        public static JsRuntimeInvocation VerifyInvoke(this MockJsRuntimeInvokeHandler handler, string identifier) => VerifyInvoke(handler, identifier, 1)[0];
+        /// 
+        /// Verifies that the  has been invoked one time.
+        /// 
+        /// Handler to verify against.
+        /// Identifier of invocation that should have been invoked.
+        /// A custom user message to display if the assertion fails.
+        /// The .
+        public static JsRuntimeInvocation VerifyInvoke(this MockJsRuntimeInvokeHandler handler, string identifier, string? userMessage = null)
+            => VerifyInvoke(handler, identifier, 1, userMessage)[0];
 
-        public static IReadOnlyList VerifyInvoke(this MockJsRuntimeInvokeHandler handler, string identifier, int calledTimes = 1, string? userMessage = null)
+        /// 
+        /// Verifies that the  has been invoked  times.
+        /// 
+        /// Handler to verify against.
+        /// Identifier of invocation that should have been invoked.
+        /// A custom user message to display if the assertion fails.
+        /// The .
+        public static IReadOnlyList VerifyInvoke(this MockJsRuntimeInvokeHandler handler, string identifier, int calledTimes, string? userMessage = null)
         {
             if (handler is null) throw new ArgumentNullException(nameof(handler));
             if (calledTimes < 1)
@@ -40,17 +61,20 @@ public static IReadOnlyList VerifyInvoke(this MockJsRuntime
             return invocations;
         }
 
+        /// 
+        /// Verifies that an argument 
+        /// passed to an JsRuntime invocation is an 
+        /// to the .
+        /// 
+        /// object to verify.
+        /// expected targeted element.
         public static void ShouldBeElementReferenceTo(this object actualArgument, IElement expectedTargetElement)
         {
             if (actualArgument is null) throw new ArgumentNullException(nameof(actualArgument));
             if (expectedTargetElement is null) throw new ArgumentNullException(nameof(expectedTargetElement));
 
-            if (!(actualArgument is ElementReference elmRef))
-            {
-                throw new IsTypeException(typeof(ElementReference).FullName, actualArgument.GetType().FullName);
-            }
-
-            var elmRefAttrName = Htmlizer.ToBlazorAttribute("elementreference");
+            var elmRef = Assert.IsType(actualArgument);
+            var elmRefAttrName = Htmlizer.ELEMENT_REFERENCE_ATTR_NAME;
             var expectedId = expectedTargetElement.GetAttribute(elmRefAttrName);
             if (string.IsNullOrEmpty(expectedId) || !elmRef.Id.Equals(expectedId, StringComparison.Ordinal))
             {
diff --git a/src/Mocking/MockJsRuntimeExtensions.cs b/src/Mocking/MockJsRuntimeExtensions.cs
index 1affd5f4e..83e503dee 100644
--- a/src/Mocking/MockJsRuntimeExtensions.cs
+++ b/src/Mocking/MockJsRuntimeExtensions.cs
@@ -2,8 +2,15 @@
 
 namespace Egil.RazorComponents.Testing
 {
+    /// 
+    /// Helper methods for registering the MockJsRuntime with a .
+    /// 
     public static class MockJsRuntimeExtensions
     {
+        /// 
+        /// Adds the  to the .
+        /// 
+        /// The added .
         public static MockJsRuntimeInvokeHandler AddMockJsRuntime(this TestServiceProvider serviceProvider, JsRuntimeMockMode mode = JsRuntimeMockMode.Loose)
         {
             if (serviceProvider is null) throw new ArgumentNullException(nameof(serviceProvider));
diff --git a/src/Rendering/Htmlizer.cs b/src/Rendering/Htmlizer.cs
index 71d6aa83f..270b85dac 100644
--- a/src/Rendering/Htmlizer.cs
+++ b/src/Rendering/Htmlizer.cs
@@ -10,7 +10,6 @@ namespace Egil.RazorComponents.Testing
     [SuppressMessage("Usage", "BL0006:Do not use RenderTree types", Justification = "")]
     internal class Htmlizer
     {
-        private const string BLAZOR_ATTR_PREFIX = "blazor:";
         private static readonly HtmlEncoder HtmlEncoder = HtmlEncoder.Default;
 
         private static readonly HashSet SelfClosingElements = new HashSet(StringComparer.OrdinalIgnoreCase)
@@ -18,7 +17,10 @@ internal class Htmlizer
             "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"
         };
 
-        public static bool IsBlazorAttribute(string attributeName) 
+        public const string BLAZOR_ATTR_PREFIX = "blazor:";
+        public const string ELEMENT_REFERENCE_ATTR_NAME = BLAZOR_ATTR_PREFIX + "elementreference";
+
+        public static bool IsBlazorAttribute(string attributeName)
             => attributeName.StartsWith(BLAZOR_ATTR_PREFIX, StringComparison.Ordinal);
 
         public static string ToBlazorAttribute(string attributeName)
@@ -200,9 +202,9 @@ private static int RenderAttributes(
                     return candidateIndex;
                 }
 
-                if(frame.FrameType == RenderTreeFrameType.ElementReferenceCapture)
+                if (frame.FrameType == RenderTreeFrameType.ElementReferenceCapture)
                 {
-                    result.Add($" {BLAZOR_ATTR_PREFIX}elementreference=\"{frame.AttributeName}\"");
+                    result.Add($" {ELEMENT_REFERENCE_ATTR_NAME}=\"{frame.AttributeName}\"");
                     return candidateIndex;
                 }
                 // End of addition
diff --git a/tests/Mocking/JSInterop/JsRuntimeAssertExtensionsTest.cs b/tests/Mocking/JSInterop/JsRuntimeAssertExtensionsTest.cs
new file mode 100644
index 000000000..a0b00fb18
--- /dev/null
+++ b/tests/Mocking/JSInterop/JsRuntimeAssertExtensionsTest.cs
@@ -0,0 +1,132 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using AngleSharp.Dom;
+using Egil.RazorComponents.Testing.Diffing;
+using Microsoft.AspNetCore.Components;
+using Microsoft.JSInterop;
+using Moq;
+using Shouldly;
+using Xunit;
+using Xunit.Sdk;
+
+namespace Egil.RazorComponents.Testing.Mocking.JSInterop
+{
+    public class JsRuntimeAssertExtensionsTest
+    {
+        [Fact(DisplayName = "VerifyNotInvoke throws if handler is null")]
+        public void Test001()
+        {
+            MockJsRuntimeInvokeHandler? handler = null;
+            Should.Throw(() => JsRuntimeAssertExtensions.VerifyNotInvoke(handler!, ""));
+        }
+
+        [Fact(DisplayName = "VerifyNotInvoke throws JsInvokeCountExpectedException if identifier " +
+                            "has been invoked one or more times")]
+        public async Task Test002()
+        {
+            var identifier = "test";
+            var handler = new MockJsRuntimeInvokeHandler();
+            await handler.ToJsRuntime().InvokeVoidAsync(identifier);
+
+            Should.Throw(() => handler.VerifyNotInvoke(identifier));
+        }
+
+        [Fact(DisplayName = "VerifyNotInvoke throws JsInvokeCountExpectedException if identifier " +
+                    "has been invoked one or more times, with custom error message")]
+        public async Task Test003()
+        {
+            var identifier = "test";
+            var errMsg = "HELLO WORLD";
+            var handler = new MockJsRuntimeInvokeHandler();
+            await handler.ToJsRuntime().InvokeVoidAsync(identifier);
+
+            Should.Throw(() => handler.VerifyNotInvoke(identifier, errMsg))
+                .UserMessage.ShouldEndWith(errMsg);
+        }
+
+        [Fact(DisplayName = "VerifyNotInvoke does not throw if identifier has not been invoked")]
+        public void Test004()
+        {
+            var handler = new MockJsRuntimeInvokeHandler();
+
+            handler.VerifyNotInvoke("FOOBAR");
+        }
+
+        [Fact(DisplayName = "VerifyInvoke throws if handler is null")]
+        public void Test100()
+        {
+            MockJsRuntimeInvokeHandler? handler = null;
+            Should.Throw(() => JsRuntimeAssertExtensions.VerifyInvoke(handler!, ""));
+            Should.Throw(() => JsRuntimeAssertExtensions.VerifyInvoke(handler!, "", 42));
+        }
+
+        [Fact(DisplayName = "VerifyInvoke throws invokeCount is less than 1")]
+        public void Test101()
+        {
+            var handler = new MockJsRuntimeInvokeHandler();
+
+            Should.Throw(() => handler.VerifyInvoke("", 0));
+        }
+
+        [Fact(DisplayName = "VerifyInvoke throws JsInvokeCountExpectedException when " +
+                            "invocation count doesn't match the expected")]
+        public async Task Test103()
+        {
+            var identifier = "test";
+            var handler = new MockJsRuntimeInvokeHandler();
+            await handler.ToJsRuntime().InvokeVoidAsync(identifier);
+
+            var actual = Should.Throw(() => handler.VerifyInvoke(identifier, 2));
+            actual.ExpectedInvocationCount.ShouldBe(2);
+            actual.ActualInvocationCount.ShouldBe(1);
+            actual.Identifier.ShouldBe(identifier);
+        }
+
+        [Fact(DisplayName = "VerifyInvoke returns the invocation(s) if the expected count matched")]
+        public async Task Test104()
+        {
+            var identifier = "test";
+            var handler = new MockJsRuntimeInvokeHandler();
+            await handler.ToJsRuntime().InvokeVoidAsync(identifier);
+
+            var invocations = handler.VerifyInvoke(identifier, 1);
+            invocations.ShouldBeSameAs(handler.Invocations[identifier]);
+
+            var invocation = handler.VerifyInvoke(identifier);
+            invocation.ShouldBe(handler.Invocations[identifier][0]);
+        }
+
+        [Fact(DisplayName = "ShouldBeElementReferenceTo throws if actualArgument or targeted element is null")]
+        public void Test200()
+        {
+            Should.Throw(() => JsRuntimeAssertExtensions.ShouldBeElementReferenceTo(null!, null!))
+                .ParamName.ShouldBe("actualArgument");
+            Should.Throw(() => JsRuntimeAssertExtensions.ShouldBeElementReferenceTo(string.Empty, null!))
+                .ParamName.ShouldBe("expectedTargetElement");
+        }
+
+        [Fact(DisplayName = "ShouldBeElementReferenceTo throws if actualArgument is not a ElementReference")]
+        public void Test201()
+        {
+            var obj = new object();
+            Should.Throw(() => obj.ShouldBeElementReferenceTo(Mock.Of()));
+        }
+
+        [Fact(DisplayName = "ShouldBeElementReferenceTo throws if element reference does not point to the provided element")]
+        public void Test202()
+        {
+            using var htmlParser = new TestHtmlParser();
+            var elmRef = new ElementReference(Guid.NewGuid().ToString());
+            var elm = htmlParser.Parse($"

").First() as IElement; + + Should.Throw(() => elmRef.ShouldBeElementReferenceTo(elm)); + + var elmWithoutRefAttr = htmlParser.Parse($"

").First() as IElement; + + Should.Throw(() => elmRef.ShouldBeElementReferenceTo(elmWithoutRefAttr)); + } + } +} From d077e1115a19d29b9328ae54a6712fd0c082db9a Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Thu, 16 Jan 2020 20:15:49 +0000 Subject: [PATCH 10/30] Reorganized test library --- ...amsTest.cs => ComponentTestFixtureTest.cs} | 2 +- ...ntReferencesIncludedInRenderedMarkup.razor | 10 +- ...ectImplicitRazorTestContextAvailable.razor | 8 ++ ...MethodsShouldBeCalledInExpectedOrder.razor | 94 +++++++++++++++++++ ...tAndFragmentFromRazorTestContextTest.razor | 0 .../LifeCycleTrackerTest.razor | 0 ...MethodsShouldBeCalledInExpectedOrder.razor | 47 ---------- tests/GlobalSuppressions.cs | 1 + .../JsRuntimeAssertExtensionsTest.cs | 4 +- tests/RenderComponentTest.cs | 63 +++++++++++++ tests/RenderedFragmentTest.cs | 59 +----------- tests/TestUtililities/MockingHelpers.cs | 6 +- 12 files changed, 179 insertions(+), 115 deletions(-) rename tests/{AllTypesOfParamsTest.cs => ComponentTestFixtureTest.cs} (98%) rename tests/{ => Components/TestComponentBaseTest}/BlazorElementReferencesIncludedInRenderedMarkup.razor (64%) rename tests/{ => Components/TestComponentBaseTest}/CorrectImplicitRazorTestContextAvailable.razor (92%) create mode 100644 tests/Components/TestComponentBaseTest/FixtureMethodsShouldBeCalledInExpectedOrder.razor rename tests/{ => Components/TestComponentBaseTest}/GettingCutAndFragmentFromRazorTestContextTest.razor (100%) rename tests/{ => Components/TestComponentBaseTest}/LifeCycleTrackerTest.razor (100%) delete mode 100644 tests/FixtureMethodsShouldBeCalledInExpectedOrder.razor create mode 100644 tests/RenderComponentTest.cs diff --git a/tests/AllTypesOfParamsTest.cs b/tests/ComponentTestFixtureTest.cs similarity index 98% rename from tests/AllTypesOfParamsTest.cs rename to tests/ComponentTestFixtureTest.cs index 6be21847d..6e3c24f76 100644 --- a/tests/AllTypesOfParamsTest.cs +++ b/tests/ComponentTestFixtureTest.cs @@ -12,7 +12,7 @@ namespace Egil.RazorComponents.Testing { [SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "")] - public class AllTypesOfParamsTest : ComponentTestFixture + public class ComponentTestFixtureTest : ComponentTestFixture { [Fact(DisplayName = "All types of parameters are correctly assigned to component on render")] public void Test001() diff --git a/tests/BlazorElementReferencesIncludedInRenderedMarkup.razor b/tests/Components/TestComponentBaseTest/BlazorElementReferencesIncludedInRenderedMarkup.razor similarity index 64% rename from tests/BlazorElementReferencesIncludedInRenderedMarkup.razor rename to tests/Components/TestComponentBaseTest/BlazorElementReferencesIncludedInRenderedMarkup.razor index c3fd541d9..6727165a2 100644 --- a/tests/BlazorElementReferencesIncludedInRenderedMarkup.razor +++ b/tests/Components/TestComponentBaseTest/BlazorElementReferencesIncludedInRenderedMarkup.razor @@ -1,4 +1,4 @@ -@*@inherits TestComponentBase +@inherits TestComponentBase @@ -9,12 +9,12 @@ @code { ElementReference refElm; - void Test(IRazorTestContext context) + void Test() { - var cut = context.GetFragment(); - + var cut = GetFragment(); + var html = cut.GetMarkup(); html.ShouldContain($"=\"{refElm.Id}\""); } -}*@ \ No newline at end of file +} \ No newline at end of file diff --git a/tests/CorrectImplicitRazorTestContextAvailable.razor b/tests/Components/TestComponentBaseTest/CorrectImplicitRazorTestContextAvailable.razor similarity index 92% rename from tests/CorrectImplicitRazorTestContextAvailable.razor rename to tests/Components/TestComponentBaseTest/CorrectImplicitRazorTestContextAvailable.razor index 03d04b0e4..36092886e 100644 --- a/tests/CorrectImplicitRazorTestContextAvailable.razor +++ b/tests/Components/TestComponentBaseTest/CorrectImplicitRazorTestContextAvailable.razor @@ -19,6 +19,7 @@ void Test1() { + this. GetComponentUnderTest().Find("p").TextContent.ShouldBe(dep1Expected.Name); GetFragment().ShouldNotBeNull(); } @@ -28,6 +29,13 @@ GetComponentUnderTest().Find("p").TextContent.ShouldBe(dep1Expected.Name); GetFragment().ShouldNotBeNull(); } + + Task TestAsync1() + { + GetComponentUnderTest().Find("p").TextContent.ShouldBe(dep1Expected.Name); + GetFragment().ShouldNotBeNull(); + return Task.CompletedTask; + } } diff --git a/tests/Components/TestComponentBaseTest/FixtureMethodsShouldBeCalledInExpectedOrder.razor b/tests/Components/TestComponentBaseTest/FixtureMethodsShouldBeCalledInExpectedOrder.razor new file mode 100644 index 000000000..c3601d795 --- /dev/null +++ b/tests/Components/TestComponentBaseTest/FixtureMethodsShouldBeCalledInExpectedOrder.razor @@ -0,0 +1,94 @@ +@inherits TestComponentBase + +[] { TestAsync2, TestAsync3 })> +

+ +@code{ + List callOrder = new List(); + IRazorTestContext? seenContext; + + void Setup() + { + seenContext = this; + callOrder.Add(nameof(Setup)); + callOrder.Count.ShouldBe(1); + callOrder[0].ShouldBe(nameof(Setup)); + } + + void Test1() + { + callOrder.Add(nameof(Test1)); + callOrder.Count.ShouldBe(2); + callOrder[0].ShouldBe(nameof(Setup)); + callOrder[1].ShouldBe(nameof(Test1)); + this.ShouldBe(seenContext); + } + + Task TestAsync1() + { + callOrder.Add(nameof(TestAsync1)); + callOrder.Count.ShouldBe(3); + callOrder[0].ShouldBe(nameof(Setup)); + callOrder[1].ShouldBe(nameof(Test1)); + callOrder[2].ShouldBe(nameof(TestAsync1)); + this.ShouldBe(seenContext); + return Task.CompletedTask; + } + + void Test2() + { + callOrder.Add(nameof(Test2)); + callOrder.Count.ShouldBe(4); + callOrder[0].ShouldBe(nameof(Setup)); + callOrder[1].ShouldBe(nameof(Test1)); + callOrder[2].ShouldBe(nameof(TestAsync1)); + callOrder[3].ShouldBe(nameof(Test2)); + this.ShouldBe(seenContext); + } + + void Test3() + { + callOrder.Add(nameof(Test3)); + callOrder.Count.ShouldBe(5); + callOrder[0].ShouldBe(nameof(Setup)); + callOrder[1].ShouldBe(nameof(Test1)); + callOrder[2].ShouldBe(nameof(TestAsync1)); + callOrder[3].ShouldBe(nameof(Test2)); + callOrder[4].ShouldBe(nameof(Test3)); + + this.ShouldBe(seenContext); + } + + Task TestAsync2() + { + callOrder.Add(nameof(TestAsync2)); + callOrder.Count.ShouldBe(6); + callOrder[0].ShouldBe(nameof(Setup)); + callOrder[1].ShouldBe(nameof(Test1)); + callOrder[2].ShouldBe(nameof(TestAsync1)); + callOrder[3].ShouldBe(nameof(Test2)); + callOrder[4].ShouldBe(nameof(Test3)); + callOrder[5].ShouldBe(nameof(TestAsync2)); + this.ShouldBe(seenContext); + return Task.CompletedTask; + } + + Task TestAsync3() + { + callOrder.Add(nameof(TestAsync3)); + callOrder.Count.ShouldBe(7); + callOrder[0].ShouldBe(nameof(Setup)); + callOrder[1].ShouldBe(nameof(Test1)); + callOrder[2].ShouldBe(nameof(TestAsync1)); + callOrder[3].ShouldBe(nameof(Test2)); + callOrder[4].ShouldBe(nameof(Test3)); + callOrder[5].ShouldBe(nameof(TestAsync2)); + callOrder[6].ShouldBe(nameof(TestAsync3)); + this.ShouldBe(seenContext); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/tests/GettingCutAndFragmentFromRazorTestContextTest.razor b/tests/Components/TestComponentBaseTest/GettingCutAndFragmentFromRazorTestContextTest.razor similarity index 100% rename from tests/GettingCutAndFragmentFromRazorTestContextTest.razor rename to tests/Components/TestComponentBaseTest/GettingCutAndFragmentFromRazorTestContextTest.razor diff --git a/tests/LifeCycleTrackerTest.razor b/tests/Components/TestComponentBaseTest/LifeCycleTrackerTest.razor similarity index 100% rename from tests/LifeCycleTrackerTest.razor rename to tests/Components/TestComponentBaseTest/LifeCycleTrackerTest.razor diff --git a/tests/FixtureMethodsShouldBeCalledInExpectedOrder.razor b/tests/FixtureMethodsShouldBeCalledInExpectedOrder.razor deleted file mode 100644 index 1d84d9a97..000000000 --- a/tests/FixtureMethodsShouldBeCalledInExpectedOrder.razor +++ /dev/null @@ -1,47 +0,0 @@ -@inherits TestComponentBase - - -
- -@code{ - List callOrder = new List(); - IRazorTestContext? seenContext; - - void Setup() - { - seenContext = this; - callOrder.Add(nameof(Setup)); - callOrder.Count.ShouldBe(1); - callOrder[0].ShouldBe(nameof(Setup)); - } - - void Test1() - { - callOrder.Add(nameof(Test1)); - callOrder.Count.ShouldBe(2); - callOrder[0].ShouldBe(nameof(Setup)); - callOrder[1].ShouldBe(nameof(Test1)); - this.ShouldBe(seenContext); - } - - void Test2() - { - callOrder.Add(nameof(Test2)); - callOrder.Count.ShouldBe(3); - callOrder[0].ShouldBe(nameof(Setup)); - callOrder[1].ShouldBe(nameof(Test1)); - callOrder[2].ShouldBe(nameof(Test2)); - this.ShouldBe(seenContext); - } - - void Test3() - { - callOrder.Add(nameof(Test3)); - callOrder.Count.ShouldBe(4); - callOrder[0].ShouldBe(nameof(Setup)); - callOrder[1].ShouldBe(nameof(Test1)); - callOrder[2].ShouldBe(nameof(Test2)); - callOrder[3].ShouldBe(nameof(Test3)); - this.ShouldBe(seenContext); - } -} \ No newline at end of file diff --git a/tests/GlobalSuppressions.cs b/tests/GlobalSuppressions.cs index 6e6c9ad0b..8a82ddb38 100644 --- a/tests/GlobalSuppressions.cs +++ b/tests/GlobalSuppressions.cs @@ -4,3 +4,4 @@ [assembly: SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "")] [assembly: SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "")] [assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "In tests its ok to catch the general exception type")] +[assembly: SuppressMessage("Usage", "CA2234:Pass system uri objects instead of strings", Justification = "")] diff --git a/tests/Mocking/JSInterop/JsRuntimeAssertExtensionsTest.cs b/tests/Mocking/JSInterop/JsRuntimeAssertExtensionsTest.cs index a0b00fb18..e0b0d71ee 100644 --- a/tests/Mocking/JSInterop/JsRuntimeAssertExtensionsTest.cs +++ b/tests/Mocking/JSInterop/JsRuntimeAssertExtensionsTest.cs @@ -120,11 +120,11 @@ public void Test202() { using var htmlParser = new TestHtmlParser(); var elmRef = new ElementReference(Guid.NewGuid().ToString()); - var elm = htmlParser.Parse($"

").First() as IElement; + var elm = (IElement)htmlParser.Parse($"

").First(); Should.Throw(() => elmRef.ShouldBeElementReferenceTo(elm)); - var elmWithoutRefAttr = htmlParser.Parse($"

").First() as IElement; + var elmWithoutRefAttr = (IElement)htmlParser.Parse($"

").First(); Should.Throw(() => elmRef.ShouldBeElementReferenceTo(elmWithoutRefAttr)); } diff --git a/tests/RenderComponentTest.cs b/tests/RenderComponentTest.cs new file mode 100644 index 000000000..8a1785120 --- /dev/null +++ b/tests/RenderComponentTest.cs @@ -0,0 +1,63 @@ +using Egil.RazorComponents.Testing.EventDispatchExtensions; +using Egil.RazorComponents.Testing.SampleComponents; +using Shouldly; +using Xunit; + +namespace Egil.RazorComponents.Testing +{ + public class RenderComponentTest : ComponentTestFixture + { + [Fact(DisplayName = "GetNodes should return the same instance " + + "when a render has not resulted in any changes")] + public void Test003() + { + var cut = RenderComponent(ChildContent("

")); + var initialNodes = cut.GetNodes(); + + cut.Render(); + cut.SetParametersAndRender(ChildContent("
")); + + Assert.Same(initialNodes, cut.GetNodes()); + } + + [Fact(DisplayName = "GetNodes should return new instance " + + "when a SetParametersAndRender has caused changes to DOM tree")] + public void Tets004() + { + var cut = RenderComponent(ChildContent("
")); + var initialNodes = cut.GetNodes(); + + cut.SetParametersAndRender(ChildContent("

")); + + Assert.NotSame(initialNodes, cut.GetNodes()); + cut.Find("p").ShouldNotBeNull(); + } + + [Fact(DisplayName = "GetNodes should return new instance " + + "when a Render has caused changes to DOM tree")] + public void Tets005() + { + var cut = RenderComponent(); + var initialNodes = cut.GetNodes(); + + cut.Render(); + + Assert.NotSame(initialNodes, cut.GetNodes()); + } + + [Fact(DisplayName = "GetNodes should return new instance " + + "when a event handler trigger has caused changes to DOM tree")] + public void Tets006() + { + var cut = RenderComponent(); + var initialNodes = cut.GetNodes(); + + cut.Find("button").Click(); + + Assert.NotSame(initialNodes, cut.GetNodes()); + } + + + } + +} diff --git a/tests/RenderedFragmentTest.cs b/tests/RenderedFragmentTest.cs index 2ddaf7cc6..d68252dcd 100644 --- a/tests/RenderedFragmentTest.cs +++ b/tests/RenderedFragmentTest.cs @@ -1,5 +1,4 @@ -using Egil.RazorComponents.Testing.EventDispatchExtensions; -using Egil.RazorComponents.Testing.Extensions; +using Egil.RazorComponents.Testing.Extensions; using Egil.RazorComponents.Testing.SampleComponents; using Egil.RazorComponents.Testing.SampleComponents.Data; using Shouldly; @@ -70,62 +69,6 @@ public void Test005() Assert.NotSame(initialValue, cut.GetNodes()); } - - } - - public class RenderComponentTest : ComponentTestFixture - { - [Fact(DisplayName = "GetNodes should return the same instance " + - "when a render has not resulted in any changes")] - public void Test003() - { - var cut = RenderComponent(ChildContent("

")); - var initialNodes = cut.GetNodes(); - - cut.Render(); - cut.SetParametersAndRender(ChildContent("
")); - - Assert.Same(initialNodes, cut.GetNodes()); - } - - [Fact(DisplayName = "GetNodes should return new instance " + - "when a SetParametersAndRender has caused changes to DOM tree")] - public void Tets004() - { - var cut = RenderComponent(ChildContent("
")); - var initialNodes = cut.GetNodes(); - - cut.SetParametersAndRender(ChildContent("

")); - - Assert.NotSame(initialNodes, cut.GetNodes()); - cut.Find("p").ShouldNotBeNull(); - } - - [Fact(DisplayName = "GetNodes should return new instance " + - "when a Render has caused changes to DOM tree")] - public void Tets005() - { - var cut = RenderComponent(); - var initialNodes = cut.GetNodes(); - - cut.Render(); - - Assert.NotSame(initialNodes, cut.GetNodes()); - } - - [Fact(DisplayName = "GetNodes should return new instance " + - "when a event handler trigger has caused changes to DOM tree")] - public void Tets006() - { - var cut = RenderComponent(); - var initialNodes = cut.GetNodes(); - - cut.Find("button").Click(); - - Assert.NotSame(initialNodes, cut.GetNodes()); - } - - } } diff --git a/tests/TestUtililities/MockingHelpers.cs b/tests/TestUtililities/MockingHelpers.cs index c3154ada8..8ab6fd030 100644 --- a/tests/TestUtililities/MockingHelpers.cs +++ b/tests/TestUtililities/MockingHelpers.cs @@ -25,7 +25,8 @@ public static class MockingHelpers /// An instance of . public static object ToMockInstance(this Type type) { - // Type to mock must be an interface, a delegate, or a non-sealed, non-static class. + if (type is null) throw new ArgumentNullException(nameof(type)); + if (type.IsMockable()) { var result = MockOfInfo.MakeGenericMethod(type).Invoke(null, Array.Empty()); @@ -50,6 +51,8 @@ public static object ToMockInstance(this Type type) /// public static bool IsMockable(this Type type) { + if (type is null) throw new ArgumentNullException(nameof(type)); + if (type.IsSealed) return type.IsDelegateType(); return true; @@ -62,6 +65,5 @@ public static bool IsDelegateType(this Type type) { return Equals(type, DelegateType); } - } } From ac617580abd05beec960a68f39d92a37e114ac5a Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Thu, 16 Jan 2020 20:49:14 +0000 Subject: [PATCH 11/30] Suppressing warnings in sample --- sample/src/Startup.cs | 3 +++ sample/tests/GlobalSuppressions.cs | 6 ++++++ .../RazorTestComponents/Components/AlertRazorTest.razor | 2 +- sample/tests/Tests/Components/AlertTest.cs | 2 +- 4 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 sample/tests/GlobalSuppressions.cs diff --git a/sample/src/Startup.cs b/sample/src/Startup.cs index 66e241d1a..cc6a48e25 100644 --- a/sample/src/Startup.cs +++ b/sample/src/Startup.cs @@ -9,10 +9,12 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using System.Diagnostics.CodeAnalysis; using Egil.RazorComponents.Testing.SampleApp.Data; namespace Egil.RazorComponents.Testing.SampleApp { + [SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "")] public class Startup { public Startup(IConfiguration configuration) @@ -22,6 +24,7 @@ public Startup(IConfiguration configuration) public IConfiguration Configuration { get; } + // This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) diff --git a/sample/tests/GlobalSuppressions.cs b/sample/tests/GlobalSuppressions.cs new file mode 100644 index 000000000..4d1347fed --- /dev/null +++ b/sample/tests/GlobalSuppressions.cs @@ -0,0 +1,6 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "", Scope = "member", Target = "~M:Egil.RazorComponents.Testing.SampleApp.Tests.Components.AlertTest2.Test008~System.Threading.Tasks.Task")] diff --git a/sample/tests/RazorTestComponents/Components/AlertRazorTest.razor b/sample/tests/RazorTestComponents/Components/AlertRazorTest.razor index 2223bbdd6..c5bb7fb42 100644 --- a/sample/tests/RazorTestComponents/Components/AlertRazorTest.razor +++ b/sample/tests/RazorTestComponents/Components/AlertRazorTest.razor @@ -1,7 +1,7 @@ @inherits TestComponentBase @code { - MockJsRuntimeInvokeHandler MockJsRuntime { get; set; } + MockJsRuntimeInvokeHandler MockJsRuntime { get; set; } = default!; void Setup() { diff --git a/sample/tests/Tests/Components/AlertTest.cs b/sample/tests/Tests/Components/AlertTest.cs index 9067e065b..b909c02cb 100644 --- a/sample/tests/Tests/Components/AlertTest.cs +++ b/sample/tests/Tests/Components/AlertTest.cs @@ -208,7 +208,7 @@ public void Test007() cut.MarkupMatches(string.Empty); } - [Fact(DisplayName = "Alert can be dismissed via Dismiss() mehod")] + [Fact(DisplayName = "Alert can be dismissed via Dismiss() method")] public async Task Test008() { // Arrange From 95f14fa32195543607a437e75ecc59b76980c82d Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Thu, 16 Jan 2020 20:50:20 +0000 Subject: [PATCH 12/30] Changed ITestContext to have a CreateNodes method instead of HtmlParser property --- src/Asserting/CompareToDiffingExtensions.cs | 2 +- src/Asserting/MarkupMatchesAssertExtensions.cs | 2 +- .../ShouldBeTextChangeAssertExtensions.cs | 3 +++ src/Components/TestComponentBase.cs | 13 ++++++++----- src/Components/TestContextAdapter.cs | 7 +++++-- src/ElementNotFoundException.cs | 12 ++++++++++++ src/ITestContext.cs | 10 +++++++--- .../JSInterop/JsRuntimeAssertExtensions.cs | 1 + src/Mocking/JSInterop/JsRuntimeInvocation.cs | 3 +++ src/Rendering/RenderedFragmentBase.cs | 6 +++--- src/TestContext.cs | 10 ++++++---- tests/TestContextTest.cs | 18 ++++++++++++++++++ 12 files changed, 68 insertions(+), 19 deletions(-) create mode 100644 tests/TestContextTest.cs diff --git a/src/Asserting/CompareToDiffingExtensions.cs b/src/Asserting/CompareToDiffingExtensions.cs index 0c6919d1f..086b429c7 100644 --- a/src/Asserting/CompareToDiffingExtensions.cs +++ b/src/Asserting/CompareToDiffingExtensions.cs @@ -26,7 +26,7 @@ public static IReadOnlyList CompareTo(this IRenderedFragment actual, stri if (expected is null) throw new ArgumentNullException(nameof(expected)); var actualNodes = actual.GetNodes(); - var expectedNodes = actual.TestContext.HtmlParser.Parse(expected); + var expectedNodes = actual.TestContext.CreateNodes(expected); return actualNodes.CompareTo(expectedNodes); } diff --git a/src/Asserting/MarkupMatchesAssertExtensions.cs b/src/Asserting/MarkupMatchesAssertExtensions.cs index 87bf3a483..2a672e5ed 100644 --- a/src/Asserting/MarkupMatchesAssertExtensions.cs +++ b/src/Asserting/MarkupMatchesAssertExtensions.cs @@ -26,7 +26,7 @@ public static void MarkupMatches(this IRenderedFragment actual, string expected, if (expected is null) throw new ArgumentNullException(nameof(expected)); var actualNodes = actual.GetNodes(); - var expectedNodes = actual.TestContext.HtmlParser.Parse(expected); + var expectedNodes = actual.TestContext.CreateNodes(expected); actualNodes.MarkupMatches(expectedNodes, userMessage); } diff --git a/src/Asserting/ShouldBeTextChangeAssertExtensions.cs b/src/Asserting/ShouldBeTextChangeAssertExtensions.cs index 93c79711d..a88d40f06 100644 --- a/src/Asserting/ShouldBeTextChangeAssertExtensions.cs +++ b/src/Asserting/ShouldBeTextChangeAssertExtensions.cs @@ -10,6 +10,9 @@ namespace Egil.RazorComponents.Testing.Asserting { + /// + /// Verification helpers for text + /// public static class ShouldBeTextChangeAssertExtensions { public static void ShouldHaveSingleTextChange(this IReadOnlyList diffs, string expectedChange, string? userMessage = null) diff --git a/src/Components/TestComponentBase.cs b/src/Components/TestComponentBase.cs index 85e0d70ea..5c1a41bd8 100644 --- a/src/Components/TestComponentBase.cs +++ b/src/Components/TestComponentBase.cs @@ -3,6 +3,7 @@ using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; +using AngleSharp.Dom; using Egil.RazorComponents.Testing.Asserting; using Egil.RazorComponents.Testing.Diffing; using Microsoft.AspNetCore.Components; @@ -33,10 +34,6 @@ public override TestServiceProvider Services public override TestRenderer Renderer => _testContextAdapter.HasActiveContext ? _testContextAdapter.Renderer : base.Renderer; - /// - public override TestHtmlParser HtmlParser - => _testContextAdapter.HasActiveContext ? _testContextAdapter.HtmlParser : base.HtmlParser; - /// public TestComponentBase() { @@ -61,7 +58,7 @@ public async Task RazorTest() await ExecuteFixtureTests(container).ConfigureAwait(false); ExecuteSnapshotTests(container); } - + /// public IRenderedFragment GetComponentUnderTest() => _testContextAdapter.GetComponentUnderTest(); @@ -78,6 +75,12 @@ public IRenderedFragment GetFragment(string? id = null) public IRenderedComponent GetFragment(string? id = null) where TComponent : class, IComponent => _testContextAdapter.GetFragment(id); + /// + public override INodeList CreateNodes(string markup) + => _testContextAdapter.HasActiveContext + ? _testContextAdapter.CreateNodes(markup) + : base.CreateNodes(markup); + /// public override IRenderedComponent RenderComponent(params ComponentParameter[] parameters) => _testContextAdapter.HasActiveContext diff --git a/src/Components/TestContextAdapter.cs b/src/Components/TestContextAdapter.cs index 3a06f1142..c172f760f 100644 --- a/src/Components/TestContextAdapter.cs +++ b/src/Components/TestContextAdapter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using AngleSharp.Dom; using Egil.RazorComponents.Testing.Diffing; using Microsoft.AspNetCore.Components; @@ -14,8 +15,6 @@ internal sealed class TestContextAdapter : IDisposable public TestRenderer Renderer => _testContext?.Renderer ?? throw new InvalidOperationException("No active test context in the adapter"); - public TestHtmlParser HtmlParser => _testContext?.HtmlParser ?? throw new InvalidOperationException("No active test context in the adapter"); - public bool HasActiveContext => !(_testContext is null); public SnapshotTestContext ActivateSnapshotTestContext(IReadOnlyList testData) @@ -41,6 +40,7 @@ public RazorTestContext ActivateRazorTestContext(IReadOnlyList tes public void Dispose() { _testContext?.Dispose(); + _razorTestContext?.Dispose(); _testContext = null; _razorTestContext = null; } @@ -67,5 +67,8 @@ public void WaitForNextRender(Action renderTrigger, TimeSpan? timeout = null) public IRenderedComponent RenderComponent(params ComponentParameter[] parameters) where TComponent : class, IComponent => _testContext?.RenderComponent(parameters) ?? throw new InvalidOperationException("No active test context in the adapter"); + + public INodeList CreateNodes(string markup) + => _testContext?.CreateNodes(markup) ?? throw new InvalidOperationException("No active test context in the adapter"); } } diff --git a/src/ElementNotFoundException.cs b/src/ElementNotFoundException.cs index 6be892897..1a3caf9fe 100644 --- a/src/ElementNotFoundException.cs +++ b/src/ElementNotFoundException.cs @@ -23,5 +23,17 @@ public ElementNotFoundException(string cssSelector) : base($"No elements were fo { CssSelector = cssSelector; } + + /// + public ElementNotFoundException() + { + CssSelector = string.Empty; + } + + /// + public ElementNotFoundException(string message, Exception innerException) : base(message, innerException) + { + CssSelector = string.Empty; + } } } diff --git a/src/ITestContext.cs b/src/ITestContext.cs index cc76e7135..e1bcd76a6 100644 --- a/src/ITestContext.cs +++ b/src/ITestContext.cs @@ -1,4 +1,5 @@ using System; +using AngleSharp.Dom; using Egil.RazorComponents.Testing.Diffing; using Microsoft.AspNetCore.Components; @@ -19,11 +20,14 @@ public interface ITestContext : IDisposable /// Gets the renderer used to render the components and fragments in this test context. /// TestRenderer Renderer { get; } - + /// - /// Gets the HTML parser used to parse HTML produced by components and fragments in this test context. + /// Parses a markup HTML string using the AngleSharps HTML5 parser + /// and returns a list of nodes. /// - TestHtmlParser HtmlParser { get; } + /// The markup to parse. + /// The . + INodeList CreateNodes(string markup); /// /// Instantiates and performs a first render of a component of type . diff --git a/src/Mocking/JSInterop/JsRuntimeAssertExtensions.cs b/src/Mocking/JSInterop/JsRuntimeAssertExtensions.cs index b2786e180..5467c0f7d 100644 --- a/src/Mocking/JSInterop/JsRuntimeAssertExtensions.cs +++ b/src/Mocking/JSInterop/JsRuntimeAssertExtensions.cs @@ -43,6 +43,7 @@ public static JsRuntimeInvocation VerifyInvoke(this MockJsRuntimeInvokeHandler h /// /// Handler to verify against. /// Identifier of invocation that should have been invoked. + /// The number of times the invocation is expected to have been called. /// A custom user message to display if the assertion fails. /// The . public static IReadOnlyList VerifyInvoke(this MockJsRuntimeInvokeHandler handler, string identifier, int calledTimes, string? userMessage = null) diff --git a/src/Mocking/JSInterop/JsRuntimeInvocation.cs b/src/Mocking/JSInterop/JsRuntimeInvocation.cs index a6437e647..68666f5ee 100644 --- a/src/Mocking/JSInterop/JsRuntimeInvocation.cs +++ b/src/Mocking/JSInterop/JsRuntimeInvocation.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System; +using System.Diagnostics.CodeAnalysis; using System.Threading; namespace Egil.RazorComponents.Testing @@ -7,6 +8,7 @@ namespace Egil.RazorComponents.Testing /// /// Represents an invocation of JavaScript via the JsRuntime Mock /// + [SuppressMessage("Design", "CA1068:CancellationToken parameters must come last", Justification = "")] public readonly struct JsRuntimeInvocation : IEquatable { /// @@ -24,6 +26,7 @@ namespace Egil.RazorComponents.Testing /// public IReadOnlyList Arguments { get; } + /// /// Creates an instance of the . /// diff --git a/src/Rendering/RenderedFragmentBase.cs b/src/Rendering/RenderedFragmentBase.cs index 7ffbbbb29..e90be618d 100644 --- a/src/Rendering/RenderedFragmentBase.cs +++ b/src/Rendering/RenderedFragmentBase.cs @@ -66,7 +66,7 @@ public IReadOnlyList GetChangesSinceSnapshot() throw new InvalidOperationException($"No snapshot exists to compare with. Call {nameof(SaveSnapshot)} to create one."); if(_snapshotNodes is null) - _snapshotNodes = TestContext.HtmlParser.Parse(_snapshotMarkup); + _snapshotNodes = TestContext.CreateNodes(_snapshotMarkup); return GetNodes().CompareTo(_snapshotNodes); } @@ -76,7 +76,7 @@ public IReadOnlyList GetChangesSinceSnapshot() public IReadOnlyList GetChangesSinceFirstRender() { if (_firstRenderNodes is null) - _firstRenderNodes = TestContext.HtmlParser.Parse(FirstRenderMarkup); + _firstRenderNodes = TestContext.CreateNodes(FirstRenderMarkup); return GetNodes().CompareTo(_firstRenderNodes); } @@ -93,7 +93,7 @@ public string GetMarkup() public INodeList GetNodes() { if (_latestRenderNodes is null) - _latestRenderNodes = TestContext.HtmlParser.Parse(GetMarkup()); + _latestRenderNodes = TestContext.CreateNodes(GetMarkup()); return _latestRenderNodes; } diff --git a/src/TestContext.cs b/src/TestContext.cs index 85561e2ce..4254e88f6 100644 --- a/src/TestContext.cs +++ b/src/TestContext.cs @@ -1,4 +1,5 @@ -using Egil.RazorComponents.Testing.Diffing; +using AngleSharp.Dom; +using Egil.RazorComponents.Testing.Diffing; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.Extensions.DependencyInjection; @@ -23,9 +24,6 @@ public class TestContext : ITestContext, IDisposable /// public virtual TestRenderer Renderer => _renderer.Value; - /// - public virtual TestHtmlParser HtmlParser => _htmlParser.Value; - /// public virtual TestServiceProvider Services { get; } = new TestServiceProvider(); @@ -45,6 +43,10 @@ public TestContext() }); } + /// + public virtual INodeList CreateNodes(string markup) + => _htmlParser.Value.Parse(markup); + /// public virtual IRenderedComponent RenderComponent(params ComponentParameter[] parameters) where TComponent : class, IComponent { diff --git a/tests/TestContextTest.cs b/tests/TestContextTest.cs new file mode 100644 index 000000000..268db66a6 --- /dev/null +++ b/tests/TestContextTest.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Egil.RazorComponents.Testing +{ + public class TestContextTest + { + [Fact(DisplayName = "")] + public void MyTestMethod() + { + throw new NotImplementedException(); + } + } +} From 751c43d469184ec7405865e62dd8d219ff1c6694 Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Thu, 16 Jan 2020 20:52:51 +0000 Subject: [PATCH 13/30] Removed empty test --- tests/TestContextTest.cs | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 tests/TestContextTest.cs diff --git a/tests/TestContextTest.cs b/tests/TestContextTest.cs deleted file mode 100644 index 268db66a6..000000000 --- a/tests/TestContextTest.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Xunit; - -namespace Egil.RazorComponents.Testing -{ - public class TestContextTest - { - [Fact(DisplayName = "")] - public void MyTestMethod() - { - throw new NotImplementedException(); - } - } -} From e04e33155fea2ac1a31f78c32940f4dfad5db334 Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Thu, 16 Jan 2020 22:08:48 +0000 Subject: [PATCH 14/30] Added missing code documentation --- .../ShouldBeTextChangeAssertExtensions.cs | 24 +++++++++++++++++++ src/Components/ComponentUnderTest.cs | 6 ++++- src/Components/Fragment.cs | 8 +++++++ src/Components/FragmentBase.cs | 8 +++++++ src/Diffing/BlazorDiffingHelpers.cs | 6 +++++ src/Diffing/DiffMarkupFormatter.cs | 14 +++++++++++ 6 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/Asserting/ShouldBeTextChangeAssertExtensions.cs b/src/Asserting/ShouldBeTextChangeAssertExtensions.cs index a88d40f06..967b76537 100644 --- a/src/Asserting/ShouldBeTextChangeAssertExtensions.cs +++ b/src/Asserting/ShouldBeTextChangeAssertExtensions.cs @@ -15,11 +15,23 @@ namespace Egil.RazorComponents.Testing.Asserting /// public static class ShouldBeTextChangeAssertExtensions { + /// + /// Verifies that a list of diffs contains only a single change, and that change is a change to a text node. + /// + /// The list of diffs to verify against. + /// The expected text change. + /// A custom error message to show if the verification fails. public static void ShouldHaveSingleTextChange(this IReadOnlyList diffs, string expectedChange, string? userMessage = null) { DiffAssertExtensions.ShouldHaveSingleChange(diffs).ShouldBeTextChange(expectedChange, userMessage); } + /// + /// Verifies that a diff is a change to a text node. + /// + /// The diff to verify. + /// The expected text change. + /// A custom error message to show if the verification fails. public static void ShouldBeTextChange(this IDiff actualChange, string expectedChange, string? userMessage = null) { if (actualChange is null) throw new ArgumentNullException(nameof(actualChange)); @@ -32,12 +44,24 @@ public static void ShouldBeTextChange(this IDiff actualChange, string expectedCh ShouldBeTextChange(actualChange, expected, userMessage); } + /// + /// Verifies that a diff is a change to a text node. + /// + /// The diff to verify. + /// The rendered fragment containing the expected text change. + /// A custom error message to show if the verification fails. public static void ShouldBeTextChange(this IDiff actualChange, IRenderedFragment expectedChange, string? userMessage = null) { if (expectedChange is null) throw new ArgumentNullException(nameof(expectedChange)); ShouldBeTextChange(actualChange, expectedChange.GetNodes(), userMessage); } + /// + /// Verifies that a diff is a change to a text node. + /// + /// The diff to verify. + /// The node list containing the expected text change. + /// A custom error message to show if the verification fails. public static void ShouldBeTextChange(this IDiff actualChange, INodeList expectedChange, string? userMessage = null) { if (actualChange is null) throw new ArgumentNullException(nameof(actualChange)); diff --git a/src/Components/ComponentUnderTest.cs b/src/Components/ComponentUnderTest.cs index 4fccffb5a..d0d4a4375 100644 --- a/src/Components/ComponentUnderTest.cs +++ b/src/Components/ComponentUnderTest.cs @@ -4,9 +4,13 @@ namespace Egil.RazorComponents.Testing { - + /// + /// Represents a component that can be added inside a , + /// where a component under test can be defined as the child content. + /// public class ComponentUnderTest : FragmentBase { + /// public override Task SetParametersAsync(ParameterView parameters) { var result = base.SetParametersAsync(parameters); diff --git a/src/Components/Fragment.cs b/src/Components/Fragment.cs index cad3e0ad4..ea0ab79bb 100644 --- a/src/Components/Fragment.cs +++ b/src/Components/Fragment.cs @@ -2,8 +2,16 @@ namespace Egil.RazorComponents.Testing { + /// + /// Represents a component that can be added inside a , whose content + /// can be accessed in Razor-based test. + /// public class Fragment : FragmentBase { + /// + /// Gets or sets the id of the fragment. The can be used to retrieve + /// the fragment from a . + /// [Parameter] public string Id { get; set; } = string.Empty; } } diff --git a/src/Components/FragmentBase.cs b/src/Components/FragmentBase.cs index c5b83a09a..73f0897f7 100644 --- a/src/Components/FragmentBase.cs +++ b/src/Components/FragmentBase.cs @@ -4,12 +4,20 @@ namespace Egil.RazorComponents.Testing { + /// + /// Represents a fragment that can be used in or . + /// public abstract class FragmentBase : IComponent { + /// + /// Gets or sets the child content of the fragment. + /// [Parameter] public RenderFragment ChildContent { get; set; } = default!; + /// public void Attach(RenderHandle renderHandle) { } + /// public virtual Task SetParametersAsync(ParameterView parameters) { parameters.SetParameterProperties(this); diff --git a/src/Diffing/BlazorDiffingHelpers.cs b/src/Diffing/BlazorDiffingHelpers.cs index 750f1be78..24bafa366 100644 --- a/src/Diffing/BlazorDiffingHelpers.cs +++ b/src/Diffing/BlazorDiffingHelpers.cs @@ -3,8 +3,14 @@ namespace Egil.RazorComponents.Testing.Diffing { + /// + /// Blazor Dffing Helpers + /// public static class BlazorDiffingHelpers { + /// + /// Represents a diffing filter that removes all special Blazor attributes added by the /. + /// public static FilterDecision BlazorEventHandlerIdAttrFilter(in AttributeComparisonSource attrSource, FilterDecision currentDecision) { if (currentDecision == FilterDecision.Exclude) return currentDecision; diff --git a/src/Diffing/DiffMarkupFormatter.cs b/src/Diffing/DiffMarkupFormatter.cs index e2434f7f1..b1f5c303b 100644 --- a/src/Diffing/DiffMarkupFormatter.cs +++ b/src/Diffing/DiffMarkupFormatter.cs @@ -5,6 +5,9 @@ namespace Egil.RazorComponents.Testing.Diffing { + /// + /// A markup formatter, that skips any special Blazor attributes added by the /. + /// public class DiffMarkupFormatter : IMarkupFormatter { private readonly IMarkupFormatter _formatter = new PrettyMarkupFormatter() @@ -13,14 +16,22 @@ public class DiffMarkupFormatter : IMarkupFormatter Indentation = " " }; + /// public string Attribute(IAttr attribute) => Htmlizer.IsBlazorAttribute(attribute?.Name ?? string.Empty) ? string.Empty : _formatter.Attribute(attribute); + /// public string CloseTag(IElement element, bool selfClosing) => _formatter.CloseTag(element, selfClosing); + + /// public string Comment(IComment comment) => _formatter.Comment(comment); + + /// public string Doctype(IDocumentType doctype) => _formatter.Doctype(doctype); + + /// public string OpenTag(IElement element, bool selfClosing) { if(element is null) throw new ArgumentNullException(nameof(element)); @@ -39,7 +50,10 @@ public string OpenTag(IElement element, bool selfClosing) return result; } + /// public string Processing(IProcessingInstruction processing) => _formatter.Processing(processing); + + /// public string Text(ICharacterData text) => _formatter.Text(text); } } From 9d50e6ff71ebd9e54c73ca6bb8f4dd85063ca7df Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Fri, 17 Jan 2020 11:20:01 +0000 Subject: [PATCH 15/30] Moved MockJsRuntime to its own namespace --- src/Mocking/JSInterop/JsInvokeCountExpectedException.cs | 2 +- src/Mocking/JSInterop/JsRuntimeAssertExtensions.cs | 2 +- src/Mocking/JSInterop/JsRuntimeInvocation.cs | 2 +- src/Mocking/JSInterop/JsRuntimeMockMode.cs | 2 +- src/Mocking/JSInterop/JsRuntimePlannedInvocation.cs | 2 +- src/Mocking/JSInterop/MockJsRuntimeInvokeHandler.cs | 2 +- src/Mocking/JSInterop/UnplannedJsInvocationException.cs | 2 +- src/Mocking/MockJsRuntimeExtensions.cs | 1 + tests/Mocking/JSInterop/MockJsRuntimeInvokeHandlerTest.cs | 2 +- 9 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Mocking/JSInterop/JsInvokeCountExpectedException.cs b/src/Mocking/JSInterop/JsInvokeCountExpectedException.cs index 6c3468c4d..058902d5a 100644 --- a/src/Mocking/JSInterop/JsInvokeCountExpectedException.cs +++ b/src/Mocking/JSInterop/JsInvokeCountExpectedException.cs @@ -1,6 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; -using Egil.RazorComponents.Testing; +using Egil.RazorComponents.Testing.Mocking.JSInterop; using Xunit.Sdk; namespace Xunit.Sdk diff --git a/src/Mocking/JSInterop/JsRuntimeAssertExtensions.cs b/src/Mocking/JSInterop/JsRuntimeAssertExtensions.cs index 5467c0f7d..e78882cfa 100644 --- a/src/Mocking/JSInterop/JsRuntimeAssertExtensions.cs +++ b/src/Mocking/JSInterop/JsRuntimeAssertExtensions.cs @@ -6,7 +6,7 @@ using Xunit; using Xunit.Sdk; -namespace Egil.RazorComponents.Testing +namespace Egil.RazorComponents.Testing.Mocking.JSInterop { /// /// Assert extensions for JsRuntimeMock diff --git a/src/Mocking/JSInterop/JsRuntimeInvocation.cs b/src/Mocking/JSInterop/JsRuntimeInvocation.cs index 68666f5ee..117953aa1 100644 --- a/src/Mocking/JSInterop/JsRuntimeInvocation.cs +++ b/src/Mocking/JSInterop/JsRuntimeInvocation.cs @@ -3,7 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Threading; -namespace Egil.RazorComponents.Testing +namespace Egil.RazorComponents.Testing.Mocking.JSInterop { /// /// Represents an invocation of JavaScript via the JsRuntime Mock diff --git a/src/Mocking/JSInterop/JsRuntimeMockMode.cs b/src/Mocking/JSInterop/JsRuntimeMockMode.cs index d36c6c799..8ce12d40c 100644 --- a/src/Mocking/JSInterop/JsRuntimeMockMode.cs +++ b/src/Mocking/JSInterop/JsRuntimeMockMode.cs @@ -1,6 +1,6 @@ using Microsoft.JSInterop; -namespace Egil.RazorComponents.Testing +namespace Egil.RazorComponents.Testing.Mocking.JSInterop { /// /// The execution mode of the . diff --git a/src/Mocking/JSInterop/JsRuntimePlannedInvocation.cs b/src/Mocking/JSInterop/JsRuntimePlannedInvocation.cs index 02037e296..8e302e748 100644 --- a/src/Mocking/JSInterop/JsRuntimePlannedInvocation.cs +++ b/src/Mocking/JSInterop/JsRuntimePlannedInvocation.cs @@ -3,7 +3,7 @@ using System; using System.Diagnostics.CodeAnalysis; -namespace Egil.RazorComponents.Testing +namespace Egil.RazorComponents.Testing.Mocking.JSInterop { /// /// Represents a planned invocation of a JavaScript function with specific arguments. diff --git a/src/Mocking/JSInterop/MockJsRuntimeInvokeHandler.cs b/src/Mocking/JSInterop/MockJsRuntimeInvokeHandler.cs index 996da6147..14de48579 100644 --- a/src/Mocking/JSInterop/MockJsRuntimeInvokeHandler.cs +++ b/src/Mocking/JSInterop/MockJsRuntimeInvokeHandler.cs @@ -6,7 +6,7 @@ using System; using System.Linq; -namespace Egil.RazorComponents.Testing +namespace Egil.RazorComponents.Testing.Mocking.JSInterop { /// /// Represents an invoke handler for a mock of a . diff --git a/src/Mocking/JSInterop/UnplannedJsInvocationException.cs b/src/Mocking/JSInterop/UnplannedJsInvocationException.cs index c4b3b3689..04ca64494 100644 --- a/src/Mocking/JSInterop/UnplannedJsInvocationException.cs +++ b/src/Mocking/JSInterop/UnplannedJsInvocationException.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using System.Diagnostics.CodeAnalysis; -namespace Egil.RazorComponents.Testing +namespace Egil.RazorComponents.Testing.Mocking.JSInterop { /// /// Exception use to indicate that an unplanned invocation was diff --git a/src/Mocking/MockJsRuntimeExtensions.cs b/src/Mocking/MockJsRuntimeExtensions.cs index 83e503dee..2c40042f8 100644 --- a/src/Mocking/MockJsRuntimeExtensions.cs +++ b/src/Mocking/MockJsRuntimeExtensions.cs @@ -1,4 +1,5 @@ using System; +using Egil.RazorComponents.Testing.Mocking.JSInterop; namespace Egil.RazorComponents.Testing { diff --git a/tests/Mocking/JSInterop/MockJsRuntimeInvokeHandlerTest.cs b/tests/Mocking/JSInterop/MockJsRuntimeInvokeHandlerTest.cs index 3c43800c9..c2fa59009 100644 --- a/tests/Mocking/JSInterop/MockJsRuntimeInvokeHandlerTest.cs +++ b/tests/Mocking/JSInterop/MockJsRuntimeInvokeHandlerTest.cs @@ -7,7 +7,7 @@ using Shouldly; using Xunit; -namespace Egil.RazorComponents.Testing.JSInterop +namespace Egil.RazorComponents.Testing.Mocking.JSInterop { public class MockJsRuntimeInvokeHandlerTest { From a9d65f6706cd9bf4e17b559d56a7a45be689b106 Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Fri, 17 Jan 2020 15:33:23 +0000 Subject: [PATCH 16/30] Pulled sample from main solution into own solution --- Razor.Components.Testing.Library.sln | 18 +---------- sample/SampleApp.sln | 31 +++++++++++++++++++ ...rary.SampleApp.csproj => SampleApp.csproj} | 1 + sample/tests/MockForecastService.cs | 10 +++--- ...pp.Tests.csproj => SampleApp.Tests.csproj} | 2 +- sample/tests/Tests/Components/AlertTest.cs | 1 + .../Tests/Components/FocussingInputTest.cs | 1 + sample/tests/Tests/Components/TodoListTest.cs | 1 + .../tests/Tests/Components/WikiSearchTest.cs | 1 + sample/tests/_Imports.razor | 2 ++ ...omponents.Testing.Library.Template.csproj} | 0 11 files changed, 45 insertions(+), 23 deletions(-) create mode 100644 sample/SampleApp.sln rename sample/src/{Egil.RazorComponents.Testing.Library.SampleApp.csproj => SampleApp.csproj} (86%) rename sample/tests/{Egil.RazorComponents.Testing.Library.SampleApp.Tests.csproj => SampleApp.Tests.csproj} (90%) rename template/{Razor.Components.Testing.Library.Template.csproj => Egil.Razor.Components.Testing.Library.Template.csproj} (100%) diff --git a/Razor.Components.Testing.Library.sln b/Razor.Components.Testing.Library.sln index fb516bc06..e08910db2 100644 --- a/Razor.Components.Testing.Library.sln +++ b/Razor.Components.Testing.Library.sln @@ -19,13 +19,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Egil.RazorComponents.Testin EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Egil.RazorComponents.Testing.Library.Tests", "tests\Egil.RazorComponents.Testing.Library.Tests.csproj", "{04E0142A-33CC-4E30-B903-F1370D94AD8C}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample", "sample", "{26D90CB9-AF66-4F42-A16E-39D2CF69C8FB}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Egil.RazorComponents.Testing.Library.SampleApp", "sample\src\Egil.RazorComponents.Testing.Library.SampleApp.csproj", "{D1FE0F2A-D856-417E-A1FD-4ECE9C64D3AE}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Egil.RazorComponents.Testing.Library.SampleApp.Tests", "sample\tests\Egil.RazorComponents.Testing.Library.SampleApp.Tests.csproj", "{A7B05744-AA61-4F8E-8173-5DE812A4A745}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Razor.Components.Testing.Library.Template", "template\Razor.Components.Testing.Library.Template.csproj", "{FB46378D-BFB8-4C72-9CA3-0407D4665218}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Egil.Razor.Components.Testing.Library.Template", "template\Egil.Razor.Components.Testing.Library.Template.csproj", "{FB46378D-BFB8-4C72-9CA3-0407D4665218}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -41,14 +35,6 @@ Global {04E0142A-33CC-4E30-B903-F1370D94AD8C}.Debug|Any CPU.Build.0 = Debug|Any CPU {04E0142A-33CC-4E30-B903-F1370D94AD8C}.Release|Any CPU.ActiveCfg = Release|Any CPU {04E0142A-33CC-4E30-B903-F1370D94AD8C}.Release|Any CPU.Build.0 = Release|Any CPU - {D1FE0F2A-D856-417E-A1FD-4ECE9C64D3AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D1FE0F2A-D856-417E-A1FD-4ECE9C64D3AE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D1FE0F2A-D856-417E-A1FD-4ECE9C64D3AE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D1FE0F2A-D856-417E-A1FD-4ECE9C64D3AE}.Release|Any CPU.Build.0 = Release|Any CPU - {A7B05744-AA61-4F8E-8173-5DE812A4A745}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A7B05744-AA61-4F8E-8173-5DE812A4A745}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A7B05744-AA61-4F8E-8173-5DE812A4A745}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A7B05744-AA61-4F8E-8173-5DE812A4A745}.Release|Any CPU.Build.0 = Release|Any CPU {FB46378D-BFB8-4C72-9CA3-0407D4665218}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FB46378D-BFB8-4C72-9CA3-0407D4665218}.Debug|Any CPU.Build.0 = Debug|Any CPU {FB46378D-BFB8-4C72-9CA3-0407D4665218}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -60,8 +46,6 @@ Global GlobalSection(NestedProjects) = preSolution {AA96790B-67C9-4141-ACDB-037C8DC092EC} = {E006E9A4-F554-46DF-838F-812956521F64} {04E0142A-33CC-4E30-B903-F1370D94AD8C} = {C929375E-BD70-4B78-88C1-BDD1623C3365} - {D1FE0F2A-D856-417E-A1FD-4ECE9C64D3AE} = {26D90CB9-AF66-4F42-A16E-39D2CF69C8FB} - {A7B05744-AA61-4F8E-8173-5DE812A4A745} = {26D90CB9-AF66-4F42-A16E-39D2CF69C8FB} {FB46378D-BFB8-4C72-9CA3-0407D4665218} = {E006E9A4-F554-46DF-838F-812956521F64} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/sample/SampleApp.sln b/sample/SampleApp.sln new file mode 100644 index 000000000..37fd9894d --- /dev/null +++ b/sample/SampleApp.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29613.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleApp", "src\SampleApp.csproj", "{0C4F7AE0-EA8A-4ECC-9003-1CEE4412BBA7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleApp.Tests", "tests\SampleApp.Tests.csproj", "{04F6D258-F69C-4BB5-87C5-3813C3CE33D8}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0C4F7AE0-EA8A-4ECC-9003-1CEE4412BBA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0C4F7AE0-EA8A-4ECC-9003-1CEE4412BBA7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0C4F7AE0-EA8A-4ECC-9003-1CEE4412BBA7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0C4F7AE0-EA8A-4ECC-9003-1CEE4412BBA7}.Release|Any CPU.Build.0 = Release|Any CPU + {04F6D258-F69C-4BB5-87C5-3813C3CE33D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {04F6D258-F69C-4BB5-87C5-3813C3CE33D8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04F6D258-F69C-4BB5-87C5-3813C3CE33D8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {04F6D258-F69C-4BB5-87C5-3813C3CE33D8}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {FBE5B0F6-5496-4BC5-BB38-CF16799DCF93} + EndGlobalSection +EndGlobal diff --git a/sample/src/Egil.RazorComponents.Testing.Library.SampleApp.csproj b/sample/src/SampleApp.csproj similarity index 86% rename from sample/src/Egil.RazorComponents.Testing.Library.SampleApp.csproj rename to sample/src/SampleApp.csproj index cce78d825..8a9ad2278 100644 --- a/sample/src/Egil.RazorComponents.Testing.Library.SampleApp.csproj +++ b/sample/src/SampleApp.csproj @@ -2,6 +2,7 @@ netcoreapp3.1 + false Egil.RazorComponents.Testing.SampleApp diff --git a/sample/tests/MockForecastService.cs b/sample/tests/MockForecastService.cs index 71d96a0dc..771df16bb 100644 --- a/sample/tests/MockForecastService.cs +++ b/sample/tests/MockForecastService.cs @@ -4,10 +4,10 @@ namespace Egil.RazorComponents.Testing.SampleApp { -internal class MockForecastService : IWeatherForecastService -{ - public TaskCompletionSource Task { get; } = new TaskCompletionSource(); + internal class MockForecastService : IWeatherForecastService + { + public TaskCompletionSource Task { get; } = new TaskCompletionSource(); - public Task GetForecastAsync(DateTime startDate) => Task.Task; -} + public Task GetForecastAsync(DateTime startDate) => Task.Task; + } } diff --git a/sample/tests/Egil.RazorComponents.Testing.Library.SampleApp.Tests.csproj b/sample/tests/SampleApp.Tests.csproj similarity index 90% rename from sample/tests/Egil.RazorComponents.Testing.Library.SampleApp.Tests.csproj rename to sample/tests/SampleApp.Tests.csproj index d2e72634c..09facc247 100644 --- a/sample/tests/Egil.RazorComponents.Testing.Library.SampleApp.Tests.csproj +++ b/sample/tests/SampleApp.Tests.csproj @@ -20,7 +20,7 @@ - + diff --git a/sample/tests/Tests/Components/AlertTest.cs b/sample/tests/Tests/Components/AlertTest.cs index b909c02cb..435b9b27e 100644 --- a/sample/tests/Tests/Components/AlertTest.cs +++ b/sample/tests/Tests/Components/AlertTest.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Egil.RazorComponents.Testing.Asserting; using Egil.RazorComponents.Testing.EventDispatchExtensions; +using Egil.RazorComponents.Testing.Mocking.JSInterop; using Egil.RazorComponents.Testing.SampleApp.Components; using Egil.RazorComponents.Testing.SampleApp.Data; using Microsoft.AspNetCore.Authentication; diff --git a/sample/tests/Tests/Components/FocussingInputTest.cs b/sample/tests/Tests/Components/FocussingInputTest.cs index 98e7df81e..76d053c6c 100644 --- a/sample/tests/Tests/Components/FocussingInputTest.cs +++ b/sample/tests/Tests/Components/FocussingInputTest.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Egil.RazorComponents.Testing.Asserting; using Egil.RazorComponents.Testing.SampleApp.Components; +using Egil.RazorComponents.Testing.Mocking.JSInterop; using Xunit; namespace Egil.RazorComponents.Testing.SampleApp.CodeOnlyTests.Components diff --git a/sample/tests/Tests/Components/TodoListTest.cs b/sample/tests/Tests/Components/TodoListTest.cs index 17bc30280..1d875e584 100644 --- a/sample/tests/Tests/Components/TodoListTest.cs +++ b/sample/tests/Tests/Components/TodoListTest.cs @@ -2,6 +2,7 @@ using AngleSharp.Dom; using Egil.RazorComponents.Testing.Asserting; using Egil.RazorComponents.Testing.EventDispatchExtensions; +using Egil.RazorComponents.Testing.Mocking.JSInterop; using Egil.RazorComponents.Testing.SampleApp.Components; using Egil.RazorComponents.Testing.SampleApp.Data; using Microsoft.AspNetCore.Components; diff --git a/sample/tests/Tests/Components/WikiSearchTest.cs b/sample/tests/Tests/Components/WikiSearchTest.cs index 2d5bfd100..70bdb3292 100644 --- a/sample/tests/Tests/Components/WikiSearchTest.cs +++ b/sample/tests/Tests/Components/WikiSearchTest.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Egil.RazorComponents.Testing.Asserting; using Egil.RazorComponents.Testing.SampleApp.Components; +using Egil.RazorComponents.Testing.Mocking.JSInterop; using Shouldly; using Xunit; diff --git a/sample/tests/_Imports.razor b/sample/tests/_Imports.razor index 4b2a8d793..31350d2ff 100644 --- a/sample/tests/_Imports.razor +++ b/sample/tests/_Imports.razor @@ -2,6 +2,7 @@ @using Egil.RazorComponents.Testing @using Egil.RazorComponents.Testing.EventDispatchExtensions +@using Egil.RazorComponents.Testing.Mocking.JSInterop @using Egil.RazorComponents.Testing.Asserting @using Egil.RazorComponents.Testing.SampleApp @@ -9,5 +10,6 @@ @using Egil.RazorComponents.Testing.SampleApp.Components @using Egil.RazorComponents.Testing.SampleApp.Pages + @using Xunit @using Shouldly diff --git a/template/Razor.Components.Testing.Library.Template.csproj b/template/Egil.Razor.Components.Testing.Library.Template.csproj similarity index 100% rename from template/Razor.Components.Testing.Library.Template.csproj rename to template/Egil.Razor.Components.Testing.Library.Template.csproj From 0af8b13d4ebf7baa25b267544a40642175edc5cf Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Fri, 17 Jan 2020 15:37:33 +0000 Subject: [PATCH 17/30] Update main.yml --- .github/workflows/main.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6864fc111..167da5751 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -28,8 +28,10 @@ jobs: dotnet-version: '3.1.100' - name: Building and verifying library run: | - dotnet build -c Release /nowarn:CS1591 + dotnet build -c Release dotnet test -c Release /nowarn:CS1591 + dotnet build sample -c Release + dotnet test sample -c Release - name: Creating library package run: dotnet pack src/ -c Release -o ${GITHUB_WORKSPACE} -p:version=$VERSION /nowarn:CS1591 - name: Buidling template package From 31c4a4f5b525451868fa1cbe5150b94b25a4580e Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Mon, 20 Jan 2020 21:51:44 +0000 Subject: [PATCH 18/30] Change GetNodes and GetMarkup to Nodes and Markup properties in IRenderedFragment (#34) --- .../Components/ThemedButtonTest.razor | 1 - src/Asserting/CompareToDiffingExtensions.cs | 5 +- .../MarkupMatchesAssertExtensions.cs | 9 ++-- .../ShouldBeAdditionAssertExtensions.cs | 2 +- .../ShouldBeRemovalAssertExtensions.cs | 2 +- .../ShouldBeTextChangeAssertExtensions.cs | 2 +- src/Rendering/IRenderedFragment.cs | 10 ++-- src/Rendering/RenderedComponent.cs | 2 +- src/Rendering/RenderedFragment.cs | 2 +- src/Rendering/RenderedFragmentBase.cs | 47 ++++++++++--------- .../CompareToDiffingExtensionsTest.cs | 7 ++- tests/ComponentTestFixtureTest.cs | 8 ++-- ...ntReferencesIncludedInRenderedMarkup.razor | 2 +- ...tAndFragmentFromRazorTestContextTest.razor | 6 +-- tests/RenderComponentTest.cs | 24 +++++----- tests/RenderedFragmentTest.cs | 18 +++---- 16 files changed, 73 insertions(+), 74 deletions(-) diff --git a/sample/tests/RazorTestComponents/Components/ThemedButtonTest.razor b/sample/tests/RazorTestComponents/Components/ThemedButtonTest.razor index 4dc524c95..60cc8d91a 100644 --- a/sample/tests/RazorTestComponents/Components/ThemedButtonTest.razor +++ b/sample/tests/RazorTestComponents/Components/ThemedButtonTest.razor @@ -15,7 +15,6 @@ void Test() { var cut = GetComponentUnderTest(); - var x = cut.GetMarkup(); cut.Find("button").ClassList.ShouldContain("btn"); } } \ No newline at end of file diff --git a/src/Asserting/CompareToDiffingExtensions.cs b/src/Asserting/CompareToDiffingExtensions.cs index 086b429c7..921c732e1 100644 --- a/src/Asserting/CompareToDiffingExtensions.cs +++ b/src/Asserting/CompareToDiffingExtensions.cs @@ -25,10 +25,9 @@ public static IReadOnlyList CompareTo(this IRenderedFragment actual, stri if (actual is null) throw new ArgumentNullException(nameof(actual)); if (expected is null) throw new ArgumentNullException(nameof(expected)); - var actualNodes = actual.GetNodes(); var expectedNodes = actual.TestContext.CreateNodes(expected); - return actualNodes.CompareTo(expectedNodes); + return actual.Nodes.CompareTo(expectedNodes); } /// @@ -43,7 +42,7 @@ public static IReadOnlyList CompareTo(this IRenderedFragment actual, IRen if (actual is null) throw new ArgumentNullException(nameof(actual)); if (expected is null) throw new ArgumentNullException(nameof(expected)); - return actual.GetNodes().CompareTo(expected.GetNodes()); + return actual.Nodes.CompareTo(expected.Nodes); } /// diff --git a/src/Asserting/MarkupMatchesAssertExtensions.cs b/src/Asserting/MarkupMatchesAssertExtensions.cs index 2a672e5ed..5f55c510a 100644 --- a/src/Asserting/MarkupMatchesAssertExtensions.cs +++ b/src/Asserting/MarkupMatchesAssertExtensions.cs @@ -25,10 +25,9 @@ public static void MarkupMatches(this IRenderedFragment actual, string expected, if (actual is null) throw new ArgumentNullException(nameof(actual)); if (expected is null) throw new ArgumentNullException(nameof(expected)); - var actualNodes = actual.GetNodes(); var expectedNodes = actual.TestContext.CreateNodes(expected); - actualNodes.MarkupMatches(expectedNodes, userMessage); + actual.Nodes.MarkupMatches(expectedNodes, userMessage); } /// @@ -44,7 +43,7 @@ public static void MarkupMatches(this IRenderedFragment actual, IRenderedFragmen if (actual is null) throw new ArgumentNullException(nameof(actual)); if (expected is null) throw new ArgumentNullException(nameof(expected)); - actual.GetNodes().MarkupMatches(expected.GetNodes(), userMessage); + actual.Nodes.MarkupMatches(expected.Nodes, userMessage); } /// @@ -61,7 +60,7 @@ public static void MarkupMatches(this INodeList actual, IRenderedFragment expect if (actual is null) throw new ArgumentNullException(nameof(actual)); if (expected is null) throw new ArgumentNullException(nameof(expected)); - actual.MarkupMatches(expected.GetNodes(), userMessage); + actual.MarkupMatches(expected.Nodes, userMessage); } /// @@ -78,7 +77,7 @@ public static void MarkupMatches(this INode actual, IRenderedFragment expected, if (actual is null) throw new ArgumentNullException(nameof(actual)); if (expected is null) throw new ArgumentNullException(nameof(expected)); - actual.MarkupMatches(expected.GetNodes(), userMessage); + actual.MarkupMatches(expected.Nodes, userMessage); } /// diff --git a/src/Asserting/ShouldBeAdditionAssertExtensions.cs b/src/Asserting/ShouldBeAdditionAssertExtensions.cs index fd9aed4ca..2f57c04e6 100644 --- a/src/Asserting/ShouldBeAdditionAssertExtensions.cs +++ b/src/Asserting/ShouldBeAdditionAssertExtensions.cs @@ -55,7 +55,7 @@ public static void ShouldBeAddition(this IDiff actualChange, string expectedChan public static void ShouldBeAddition(this IDiff actualChange, IRenderedFragment expectedChange, string? userMessage = null) { if (expectedChange is null) throw new ArgumentNullException(nameof(expectedChange)); - ShouldBeAddition(actualChange, expectedChange.GetNodes(), userMessage); + ShouldBeAddition(actualChange, expectedChange.Nodes, userMessage); } /// diff --git a/src/Asserting/ShouldBeRemovalAssertExtensions.cs b/src/Asserting/ShouldBeRemovalAssertExtensions.cs index f24a79179..d75498b46 100644 --- a/src/Asserting/ShouldBeRemovalAssertExtensions.cs +++ b/src/Asserting/ShouldBeRemovalAssertExtensions.cs @@ -54,7 +54,7 @@ public static void ShouldBeRemoval(this IDiff actualChange, string expectedChang public static void ShouldBeRemoval(this IDiff actualChange, IRenderedFragment expectedChange, string? userMessage = null) { if (expectedChange is null) throw new ArgumentNullException(nameof(expectedChange)); - ShouldBeRemoval(actualChange, expectedChange.GetNodes(), userMessage); + ShouldBeRemoval(actualChange, expectedChange.Nodes, userMessage); } /// diff --git a/src/Asserting/ShouldBeTextChangeAssertExtensions.cs b/src/Asserting/ShouldBeTextChangeAssertExtensions.cs index 967b76537..e91cb17ad 100644 --- a/src/Asserting/ShouldBeTextChangeAssertExtensions.cs +++ b/src/Asserting/ShouldBeTextChangeAssertExtensions.cs @@ -53,7 +53,7 @@ public static void ShouldBeTextChange(this IDiff actualChange, string expectedCh public static void ShouldBeTextChange(this IDiff actualChange, IRenderedFragment expectedChange, string? userMessage = null) { if (expectedChange is null) throw new ArgumentNullException(nameof(expectedChange)); - ShouldBeTextChange(actualChange, expectedChange.GetNodes(), userMessage); + ShouldBeTextChange(actualChange, expectedChange.Nodes, userMessage); } /// diff --git a/src/Rendering/IRenderedFragment.cs b/src/Rendering/IRenderedFragment.cs index d60ba1c41..9742bb918 100644 --- a/src/Rendering/IRenderedFragment.cs +++ b/src/Rendering/IRenderedFragment.cs @@ -19,15 +19,13 @@ public interface IRenderedFragment /// /// Gets the HTML markup from the rendered fragment/component. /// - /// - string GetMarkup(); + string Markup { get; } /// /// Gets the AngleSharp based /// on the HTML markup from the rendered fragment/component. /// - /// - INodeList GetNodes(); + INodeList Nodes { get; } /// /// Performs a comparison of the markup produced by the initial rendering of the @@ -60,7 +58,7 @@ public interface IRenderedFragment /// The group of selectors to use. public IElement Find(string cssSelector) { - var result = GetNodes().QuerySelector(cssSelector); + var result = Nodes.QuerySelector(cssSelector); if (result is null) throw new ElementNotFoundException(cssSelector); else @@ -75,7 +73,7 @@ public IElement Find(string cssSelector) /// The group of selectors to use. public IHtmlCollection FindAll(string cssSelector) { - return GetNodes().QuerySelectorAll(cssSelector); + return Nodes.QuerySelectorAll(cssSelector); } } } \ No newline at end of file diff --git a/src/Rendering/RenderedComponent.cs b/src/Rendering/RenderedComponent.cs index f586301b7..e409120cf 100644 --- a/src/Rendering/RenderedComponent.cs +++ b/src/Rendering/RenderedComponent.cs @@ -43,7 +43,7 @@ public RenderedComponent(ITestContext testContext, RenderFragment renderFragment : base(testContext, renderFragment) { (ComponentId, Instance) = Container.GetComponent(); - FirstRenderMarkup = GetMarkup(); + FirstRenderMarkup = Markup; } /// diff --git a/src/Rendering/RenderedFragment.cs b/src/Rendering/RenderedFragment.cs index 724d74daa..130b32d5e 100644 --- a/src/Rendering/RenderedFragment.cs +++ b/src/Rendering/RenderedFragment.cs @@ -26,7 +26,7 @@ public class RenderedFragment : RenderedFragmentBase public RenderedFragment(ITestContext testContext, RenderFragment renderFragment) : base(testContext, renderFragment) { - FirstRenderMarkup = GetMarkup(); + FirstRenderMarkup = Markup; } } } diff --git a/src/Rendering/RenderedFragmentBase.cs b/src/Rendering/RenderedFragmentBase.cs index e90be618d..e02050b5b 100644 --- a/src/Rendering/RenderedFragmentBase.cs +++ b/src/Rendering/RenderedFragmentBase.cs @@ -39,6 +39,28 @@ public abstract class RenderedFragmentBase : IRenderedFragment /// public ITestContext TestContext { get; } + /// + public string Markup + { + get + { + if (_latestRenderMarkup is null) + _latestRenderMarkup = Htmlizer.GetHtml(TestContext.Renderer, ComponentId); + return _latestRenderMarkup; + } + } + + /// + public INodeList Nodes + { + get + { + if (_latestRenderNodes is null) + _latestRenderNodes = TestContext.CreateNodes(Markup); + return _latestRenderNodes; + } + } + /// /// Creates an instance of the class. /// @@ -56,7 +78,7 @@ public RenderedFragmentBase(ITestContext testContext, RenderFragment renderFragm public void SaveSnapshot() { _snapshotNodes = null; - _snapshotMarkup = GetMarkup(); + _snapshotMarkup = Markup; } /// @@ -65,10 +87,10 @@ public IReadOnlyList GetChangesSinceSnapshot() if (_snapshotMarkup is null) throw new InvalidOperationException($"No snapshot exists to compare with. Call {nameof(SaveSnapshot)} to create one."); - if(_snapshotNodes is null) + if (_snapshotNodes is null) _snapshotNodes = TestContext.CreateNodes(_snapshotMarkup); - return GetNodes().CompareTo(_snapshotNodes); + return Nodes.CompareTo(_snapshotNodes); } @@ -77,24 +99,7 @@ public IReadOnlyList GetChangesSinceFirstRender() { if (_firstRenderNodes is null) _firstRenderNodes = TestContext.CreateNodes(FirstRenderMarkup); - return GetNodes().CompareTo(_firstRenderNodes); - } - - - /// - public string GetMarkup() - { - if (_latestRenderMarkup is null) - _latestRenderMarkup = Htmlizer.GetHtml(TestContext.Renderer, ComponentId); - return _latestRenderMarkup; - } - - /// - public INodeList GetNodes() - { - if (_latestRenderNodes is null) - _latestRenderNodes = TestContext.CreateNodes(GetMarkup()); - return _latestRenderNodes; + return Nodes.CompareTo(_firstRenderNodes); } private void ComponentMarkupChanged(in RenderBatch renderBatch) diff --git a/tests/Asserting/CompareToDiffingExtensionsTest.cs b/tests/Asserting/CompareToDiffingExtensionsTest.cs index 7eb34e870..6f2c60448 100644 --- a/tests/Asserting/CompareToDiffingExtensionsTest.cs +++ b/tests/Asserting/CompareToDiffingExtensionsTest.cs @@ -54,7 +54,7 @@ public void Test002() var rf1 = RenderComponent((nameof(Simple1.Header), "FOO")); var rf2 = RenderComponent((nameof(Simple1.Header), "BAR")); - rf1.CompareTo(rf2.GetMarkup()).Count.ShouldBe(1); + rf1.CompareTo(rf2.Markup).Count.ShouldBe(1); } [Fact(DisplayName = "CompareTo with rendered fragment and rendered fragment")] @@ -73,10 +73,9 @@ public void Test004() var rf2 = RenderComponent((nameof(Simple1.Header), "BAR")); var elm = rf1.Find("h1"); - elm.CompareTo(rf2.GetNodes()).Count.ShouldBe(1); + elm.CompareTo(rf2.Nodes).Count.ShouldBe(1); } - [Fact(DisplayName = "CompareTo with INodeList and INode")] public void Test005() { @@ -84,7 +83,7 @@ public void Test005() var rf2 = RenderComponent((nameof(Simple1.Header), "BAR")); var elm = rf1.Find("h1"); - rf2.GetNodes().CompareTo(elm).Count.ShouldBe(1); + rf2.Nodes.CompareTo(elm).Count.ShouldBe(1); } } } diff --git a/tests/ComponentTestFixtureTest.cs b/tests/ComponentTestFixtureTest.cs index 6e3c24f76..fd5ec5e35 100644 --- a/tests/ComponentTestFixtureTest.cs +++ b/tests/ComponentTestFixtureTest.cs @@ -39,8 +39,8 @@ public void Test001() instance.NamedCascadingValue.ShouldBe(1337); Should.Throw(async () => await instance.NonGenericCallback.InvokeAsync(null)).Message.ShouldBe("NonGenericCallback"); Should.Throw(async () => await instance.GenericCallback.InvokeAsync(EventArgs.Empty)).Message.ShouldBe("GenericCallback"); - new RenderedFragment(this, instance.ChildContent!).GetMarkup().ShouldBe(nameof(ChildContent)); - new RenderedFragment(this, instance.OtherContent!).GetMarkup().ShouldBe(nameof(AllTypesOfParams.OtherContent)); + new RenderedFragment(this, instance.ChildContent!).Markup.ShouldBe(nameof(ChildContent)); + new RenderedFragment(this, instance.OtherContent!).Markup.ShouldBe(nameof(AllTypesOfParams.OtherContent)); Should.Throw(() => instance.ItemTemplate!("")(null)).Message.ShouldBe("ItemTemplate"); } @@ -78,8 +78,8 @@ public void Test002() instance.RegularParam.ShouldBe("some value"); Should.Throw(async () => await instance.NonGenericCallback.InvokeAsync(null)).Message.ShouldBe("NonGenericCallback"); Should.Throw(async () => await instance.GenericCallback.InvokeAsync(EventArgs.Empty)).Message.ShouldBe("GenericCallback"); - new RenderedFragment(this, instance.ChildContent!).GetMarkup().ShouldBe(nameof(ChildContent)); - new RenderedFragment(this, instance.OtherContent!).GetMarkup().ShouldBe(nameof(AllTypesOfParams.OtherContent)); + new RenderedFragment(this, instance.ChildContent!).Markup.ShouldBe(nameof(ChildContent)); + new RenderedFragment(this, instance.OtherContent!).Markup.ShouldBe(nameof(AllTypesOfParams.OtherContent)); Should.Throw(() => instance.ItemTemplate!("")(null)).Message.ShouldBe("ItemTemplate"); } diff --git a/tests/Components/TestComponentBaseTest/BlazorElementReferencesIncludedInRenderedMarkup.razor b/tests/Components/TestComponentBaseTest/BlazorElementReferencesIncludedInRenderedMarkup.razor index 6727165a2..3133f8108 100644 --- a/tests/Components/TestComponentBaseTest/BlazorElementReferencesIncludedInRenderedMarkup.razor +++ b/tests/Components/TestComponentBaseTest/BlazorElementReferencesIncludedInRenderedMarkup.razor @@ -13,7 +13,7 @@ { var cut = GetFragment(); - var html = cut.GetMarkup(); + var html = cut.Markup; html.ShouldContain($"=\"{refElm.Id}\""); } diff --git a/tests/Components/TestComponentBaseTest/GettingCutAndFragmentFromRazorTestContextTest.razor b/tests/Components/TestComponentBaseTest/GettingCutAndFragmentFromRazorTestContextTest.razor index 5af2ceb24..fc27ce7f0 100644 --- a/tests/Components/TestComponentBaseTest/GettingCutAndFragmentFromRazorTestContextTest.razor +++ b/tests/Components/TestComponentBaseTest/GettingCutAndFragmentFromRazorTestContextTest.razor @@ -16,7 +16,7 @@ var cut2 = GetComponentUnderTest(); Assert.True(ReferenceEquals(cut1, cut2), "Getting CUT multiple times should return the same instance"); - Assert.Equal("CUT", cut1.GetMarkup()); + Assert.Equal("CUT", cut1.Markup); var firstFragmentNoId1 = GetFragment(); var firstFragmentId1 = GetFragment("first"); @@ -25,13 +25,13 @@ Assert.True(ReferenceEquals(firstFragmentNoId1, firstFragmentId1), "Getting first fragment with and without id should return the same instance"); Assert.True(ReferenceEquals(firstFragmentNoId1, firstFragmentNoId2), "Getting first fragment multiple times should return the same instance"); Assert.True(ReferenceEquals(firstFragmentId1, firstFragmentId2), "Getting first fragment multiple times should return the same instance"); - Assert.Equal("first", firstFragmentNoId1.GetMarkup()); + Assert.Equal("first", firstFragmentNoId1.Markup); var secondFragmentId1 = GetFragment("second"); var secondFragmentId2 = GetFragment("second"); Assert.True(ReferenceEquals(secondFragmentId1, secondFragmentId2), "Getting fragment multiple times should return the same instance"); - Assert.Equal("second", secondFragmentId2.GetMarkup()); + Assert.Equal("second", secondFragmentId2.Markup); } } diff --git a/tests/RenderComponentTest.cs b/tests/RenderComponentTest.cs index 8a1785120..33379530d 100644 --- a/tests/RenderComponentTest.cs +++ b/tests/RenderComponentTest.cs @@ -7,54 +7,54 @@ namespace Egil.RazorComponents.Testing { public class RenderComponentTest : ComponentTestFixture { - [Fact(DisplayName = "GetNodes should return the same instance " + + [Fact(DisplayName = "Nodes should return the same instance " + "when a render has not resulted in any changes")] public void Test003() { var cut = RenderComponent(ChildContent("
")); - var initialNodes = cut.GetNodes(); + var initialNodes = cut.Nodes; cut.Render(); cut.SetParametersAndRender(ChildContent("
")); - Assert.Same(initialNodes, cut.GetNodes()); + Assert.Same(initialNodes, cut.Nodes); } - [Fact(DisplayName = "GetNodes should return new instance " + + [Fact(DisplayName = "Nodes should return new instance " + "when a SetParametersAndRender has caused changes to DOM tree")] public void Tets004() { var cut = RenderComponent(ChildContent("
")); - var initialNodes = cut.GetNodes(); + var initialNodes = cut.Nodes; cut.SetParametersAndRender(ChildContent("

")); - Assert.NotSame(initialNodes, cut.GetNodes()); + Assert.NotSame(initialNodes, cut.Nodes); cut.Find("p").ShouldNotBeNull(); } - [Fact(DisplayName = "GetNodes should return new instance " + + [Fact(DisplayName = "Nodes should return new instance " + "when a Render has caused changes to DOM tree")] public void Tets005() { var cut = RenderComponent(); - var initialNodes = cut.GetNodes(); + var initialNodes = cut.Nodes; cut.Render(); - Assert.NotSame(initialNodes, cut.GetNodes()); + Assert.NotSame(initialNodes, cut.Nodes); } - [Fact(DisplayName = "GetNodes should return new instance " + + [Fact(DisplayName = "Nodes should return new instance " + "when a event handler trigger has caused changes to DOM tree")] public void Tets006() { var cut = RenderComponent(); - var initialNodes = cut.GetNodes(); + var initialNodes = cut.Nodes; cut.Find("button").Click(); - Assert.NotSame(initialNodes, cut.GetNodes()); + Assert.NotSame(initialNodes, cut.Nodes); } diff --git a/tests/RenderedFragmentTest.cs b/tests/RenderedFragmentTest.cs index d68252dcd..ae5474a8f 100644 --- a/tests/RenderedFragmentTest.cs +++ b/tests/RenderedFragmentTest.cs @@ -27,47 +27,47 @@ public void Test002() result.ShouldNotBeNull(); } - [Fact(DisplayName = "GetNodes should return new instance when " + + [Fact(DisplayName = "Nodes should return new instance when " + "async operation during OnInit causes component to re-render")] public void Test003() { var testData = new AsyncNameDep(); Services.AddService(testData); var cut = RenderComponent(); - var initialValue = cut.GetNodes().Find("p").OuterHtml; + var initialValue = cut.Nodes.Find("p").OuterHtml; WaitForNextRender(() => testData.SetResult("Steve Sanderson")); - var steveValue = cut.GetNodes().Find("p").OuterHtml; + var steveValue = cut.Nodes.Find("p").OuterHtml; steveValue.ShouldNotBe(initialValue); } - [Fact(DisplayName = "GetNodes should return new instance when " + + [Fact(DisplayName = "Nodes should return new instance when " + "async operation/StateHasChanged during OnAfterRender causes component to re-render")] public void Test004() { var invocation = Services.AddMockJsRuntime().Setup("getdata"); var cut = RenderComponent(); - var initialValue = cut.GetNodes().Find("p").OuterHtml; + var initialValue = cut.Nodes.Find("p").OuterHtml; WaitForNextRender(() => invocation.SetResult("Steve Sanderson")); - var steveValue = cut.GetNodes().Find("p").OuterHtml; + var steveValue = cut.Nodes.Find("p").OuterHtml; steveValue.ShouldNotBe(initialValue); } - [Fact(DisplayName = "GetNodes on a components with child component returns " + + [Fact(DisplayName = "Nodes on a components with child component returns " + "new instance when the child component has changes")] public void Test005() { var invocation = Services.AddMockJsRuntime().Setup("getdata"); var notcut = RenderComponent(ChildContent()); var cut = RenderComponent(ChildContent()); - var initialValue = cut.GetNodes(); + var initialValue = cut.Nodes; WaitForNextRender(() => invocation.SetResult("Steve Sanderson"), TimeSpan.FromDays(1)); - Assert.NotSame(initialValue, cut.GetNodes()); + Assert.NotSame(initialValue, cut.Nodes); } } From c78be10d44033ec92089ec61f8da0849f054754b Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Tue, 21 Jan 2020 21:46:37 +0000 Subject: [PATCH 19/30] Add SetupVoid and SetVoidResult capabilities to JsRuntime mock (#35) * Moved MockJsRuntime to its own namespace * Add SetupVoid() and SetVoidResult() to PlannedInvocation and Mock * Updates to template and sample --- .../Tests/Components/FocussingInputTest.cs | 2 + sample/tests/Tests/Components/TodoListTest.cs | 1 + sample/tests/_Imports.razor | 1 + .../JSInterop/JsRuntimePlannedInvocation.cs | 75 ++++++- .../JSInterop/MockJsRuntimeInvokeHandler.cs | 42 +++- .../UnplannedJsInvocationException.cs | 15 +- template/template/Component1Test.cs | 1 + template/template/_Imports.razor | 1 + .../MockJsRuntimeInvokeHandlerTest.cs | 196 +++++++++++++++++- 9 files changed, 305 insertions(+), 29 deletions(-) diff --git a/sample/tests/Tests/Components/FocussingInputTest.cs b/sample/tests/Tests/Components/FocussingInputTest.cs index 76d053c6c..bc84d316b 100644 --- a/sample/tests/Tests/Components/FocussingInputTest.cs +++ b/sample/tests/Tests/Components/FocussingInputTest.cs @@ -4,6 +4,7 @@ using System.Text; using System.Threading.Tasks; using Egil.RazorComponents.Testing.Asserting; +using Egil.RazorComponents.Testing.Mocking.JSInterop; using Egil.RazorComponents.Testing.SampleApp.Components; using Egil.RazorComponents.Testing.Mocking.JSInterop; using Xunit; @@ -25,6 +26,7 @@ public void Test001() // Assert // that there is a single call to document.body.focus.call var invocation = jsRtMock.VerifyInvoke("document.body.focus.call"); + // Assert that the invocation received a single argument // and that it was a reference to the input element. var expectedReferencedElement = cut.Find("input"); diff --git a/sample/tests/Tests/Components/TodoListTest.cs b/sample/tests/Tests/Components/TodoListTest.cs index 1d875e584..a0946c64b 100644 --- a/sample/tests/Tests/Components/TodoListTest.cs +++ b/sample/tests/Tests/Components/TodoListTest.cs @@ -1,6 +1,7 @@ using Shouldly; using AngleSharp.Dom; using Egil.RazorComponents.Testing.Asserting; +using Egil.RazorComponents.Testing.Mocking.JSInterop; using Egil.RazorComponents.Testing.EventDispatchExtensions; using Egil.RazorComponents.Testing.Mocking.JSInterop; using Egil.RazorComponents.Testing.SampleApp.Components; diff --git a/sample/tests/_Imports.razor b/sample/tests/_Imports.razor index 31350d2ff..94b4791dd 100644 --- a/sample/tests/_Imports.razor +++ b/sample/tests/_Imports.razor @@ -4,6 +4,7 @@ @using Egil.RazorComponents.Testing.EventDispatchExtensions @using Egil.RazorComponents.Testing.Mocking.JSInterop @using Egil.RazorComponents.Testing.Asserting +@using Egil.RazorComponents.Testing.Mocking.JSInterop @using Egil.RazorComponents.Testing.SampleApp @using Egil.RazorComponents.Testing.SampleApp.Data diff --git a/src/Mocking/JSInterop/JsRuntimePlannedInvocation.cs b/src/Mocking/JSInterop/JsRuntimePlannedInvocation.cs index 8e302e748..4c3a796c1 100644 --- a/src/Mocking/JSInterop/JsRuntimePlannedInvocation.cs +++ b/src/Mocking/JSInterop/JsRuntimePlannedInvocation.cs @@ -5,16 +5,55 @@ namespace Egil.RazorComponents.Testing.Mocking.JSInterop { + ///

+ /// Represents a planned invocation of a JavaScript function which returns nothing, with specific arguments. + /// + public class JsRuntimePlannedInvocation : JsRuntimePlannedInvocationBase + { + internal JsRuntimePlannedInvocation(string identifier, Func, bool> matcher) : base(identifier, matcher) + { + } + + /// + /// Completes the current awaiting void invocation requests. + /// + public void SetVoidResult() + { + base.SetResultBase(default!); + } + } + + /// + /// Represents a planned invocation of a JavaScript function with specific arguments. + /// + /// + public class JsRuntimePlannedInvocation : JsRuntimePlannedInvocationBase + { + internal JsRuntimePlannedInvocation(string identifier, Func, bool> matcher) : base(identifier, matcher) + { + } + + /// + /// Sets the result that invocations will receive. + /// + /// + public void SetResult(TResult result) + { + base.SetResultBase(result); + } + } + /// /// Represents a planned invocation of a JavaScript function with specific arguments. /// /// - [SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "")] - public readonly struct JsRuntimePlannedInvocation + public abstract class JsRuntimePlannedInvocationBase { private readonly List _invocations; + private Func, bool> InvocationMatcher { get; } - internal TaskCompletionSource CompletionSource { get; } + + private TaskCompletionSource _completionSource; /// /// The expected identifier for the function to invoke. @@ -26,36 +65,52 @@ public readonly struct JsRuntimePlannedInvocation /// public IReadOnlyList Invocations => _invocations.AsReadOnly(); - internal JsRuntimePlannedInvocation(string identifier, Func, bool> matcher) + /// + /// Creates an instance of a . + /// + protected JsRuntimePlannedInvocationBase(string identifier, Func, bool> matcher) { Identifier = identifier; _invocations = new List(); InvocationMatcher = matcher; - CompletionSource = new TaskCompletionSource(); + _completionSource = new TaskCompletionSource(); } /// /// Sets the result that invocations will receive. /// /// - public void SetResult(TResult result) => CompletionSource.SetResult(result); + protected void SetResultBase(TResult result) + { + _completionSource.SetResult(result); + } /// /// Sets the exception that invocations will receive. /// /// - public void SetException(TException exception) + public void SetException(TException exception) where TException : Exception - => CompletionSource.SetException(exception); + { + _completionSource.SetException(exception); + } /// /// Marks the that invocations will receive as canceled. /// - public void SetCanceled() => CompletionSource.SetCanceled(); + public void SetCanceled() + { + _completionSource.SetCanceled(); + } - internal void AddInvocation(JsRuntimeInvocation invocation) + internal Task RegisterInvocation(JsRuntimeInvocation invocation) { + if (_completionSource.Task.IsCompleted) + _completionSource = new TaskCompletionSource(); + _invocations.Add(invocation); + + return _completionSource.Task; } internal bool Matches(JsRuntimeInvocation invocation) diff --git a/src/Mocking/JSInterop/MockJsRuntimeInvokeHandler.cs b/src/Mocking/JSInterop/MockJsRuntimeInvokeHandler.cs index 14de48579..51f9efb0c 100644 --- a/src/Mocking/JSInterop/MockJsRuntimeInvokeHandler.cs +++ b/src/Mocking/JSInterop/MockJsRuntimeInvokeHandler.cs @@ -52,7 +52,7 @@ public IJSRuntime ToJsRuntime() /// The result type of the invocation /// The identifier to setup a response for /// A matcher that is passed arguments received in invocations to . If it returns true the invocation is matched. - /// A whose is returned when the is invoked. + /// A . public JsRuntimePlannedInvocation Setup(string identifier, Func, bool> argumentsMatcher) { var result = new JsRuntimePlannedInvocation(identifier, argumentsMatcher); @@ -68,13 +68,41 @@ public JsRuntimePlannedInvocation Setup(string identifier, Fun /// /// The identifier to setup a response for /// The arguments that an invocation to should match. - /// A whose is returned when the is invoked. + /// A . public JsRuntimePlannedInvocation Setup(string identifier, params object[] arguments) { return Setup(identifier, args => Enumerable.SequenceEqual(args, arguments)); } - private void AddPlannedInvocation(JsRuntimePlannedInvocation planned) + /// + /// Configure a planned JSInterop invocation with the and arguments + /// passing the test, that should not receive any result. + /// + /// The identifier to setup a response for + /// A matcher that is passed arguments received in invocations to . If it returns true the invocation is matched. + /// A . + public JsRuntimePlannedInvocation SetupVoid(string identifier, Func, bool> argumentsMatcher) + { + var result = new JsRuntimePlannedInvocation(identifier, argumentsMatcher); + + AddPlannedInvocation(result); + + return result; + } + + /// + /// Configure a planned JSInterop invocation with the + /// and , that should not receive any result. + /// + /// The identifier to setup a response for + /// The arguments that an invocation to should match. + /// A . + public JsRuntimePlannedInvocation SetupVoid(string identifier, params object[] arguments) + { + return SetupVoid(identifier, args => Enumerable.SequenceEqual(args, arguments)); + } + + private void AddPlannedInvocation(JsRuntimePlannedInvocationBase planned) { if (!_plannedInvocations.ContainsKey(planned.Identifier)) { @@ -119,15 +147,13 @@ public ValueTask InvokeAsync(string identifier, CancellationToke if (_handlers._plannedInvocations.TryGetValue(identifier, out var plannedInvocations)) { - var planned = plannedInvocations.OfType>() + var planned = plannedInvocations.OfType>() .SingleOrDefault(x => x.Matches(invocation)); - // TODO: Should we check the CancellationToken at this point and automatically call - // TrySetCanceled(CancellationToken) on the TaskCompletionSource? (https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskcompletionsource-1.trysetcanceled?view=netcore-3.0#System_Threading_Tasks_TaskCompletionSource_1_TrySetCanceled_System_Threading_CancellationToken_) if (planned is { }) { - planned.AddInvocation(invocation); - result = new ValueTask(planned.CompletionSource.Task); + var task = planned.RegisterInvocation(invocation); + result = new ValueTask(task); } } diff --git a/src/Mocking/JSInterop/UnplannedJsInvocationException.cs b/src/Mocking/JSInterop/UnplannedJsInvocationException.cs index 04ca64494..f1c30f3aa 100644 --- a/src/Mocking/JSInterop/UnplannedJsInvocationException.cs +++ b/src/Mocking/JSInterop/UnplannedJsInvocationException.cs @@ -25,14 +25,25 @@ public class UnplannedJsInvocationException : Exception /// /// The unplanned invocation. public UnplannedJsInvocationException(JsRuntimeInvocation invocation) - : base($"The invocation of '{invocation.Identifier} with arguments '[{PrintArguments(invocation.Arguments)}]") + : base($"The invocation of '{invocation.Identifier}' {PrintArguments(invocation.Arguments)} was not expected.") { Invocation = invocation; } private static string PrintArguments(IReadOnlyList arguments) { - return string.Join(", ", arguments.Select(x => x.ToString())); + if (arguments.Count == 0) + { + return "without arguments"; + } + else if (arguments.Count == 1) + { + return $"with the argument [{arguments[0].ToString()}]"; + } + else + { + return $"with arguments [{string.Join(", ", arguments.Select(x => x.ToString()))}]"; + } } } } diff --git a/template/template/Component1Test.cs b/template/template/Component1Test.cs index 8f72cbeaf..98d7d5f69 100644 --- a/template/template/Component1Test.cs +++ b/template/template/Component1Test.cs @@ -3,6 +3,7 @@ using Egil.RazorComponents.Testing; using Egil.RazorComponents.Testing.Diffing; using Egil.RazorComponents.Testing.Asserting; +using Egil.RazorComponents.Testing.Mocking.JSInterop; using Egil.RazorComponents.Testing.EventDispatchExtensions; namespace Company.RazorTests1 diff --git a/template/template/_Imports.razor b/template/template/_Imports.razor index 29871f1da..6b095ce66 100644 --- a/template/template/_Imports.razor +++ b/template/template/_Imports.razor @@ -3,4 +3,5 @@ @using Egil.RazorComponents.Testing.Diffing @using Egil.RazorComponents.Testing.Asserting @using Egil.RazorComponents.Testing.EventDispatchExtensions +@using Egil.RazorComponents.Testing.Mocking.JSInterop @using Xunit \ No newline at end of file diff --git a/tests/Mocking/JSInterop/MockJsRuntimeInvokeHandlerTest.cs b/tests/Mocking/JSInterop/MockJsRuntimeInvokeHandlerTest.cs index c2fa59009..9325051bc 100644 --- a/tests/Mocking/JSInterop/MockJsRuntimeInvokeHandlerTest.cs +++ b/tests/Mocking/JSInterop/MockJsRuntimeInvokeHandlerTest.cs @@ -4,6 +4,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.JSInterop; using Shouldly; using Xunit; @@ -38,24 +39,201 @@ public void Test002() } [Fact(DisplayName = "Mock throws exception when in strict mode and invocation has not been setup")] - public void Test003() + public async Task Test003() { var sut = new MockJsRuntimeInvokeHandler(JsRuntimeMockMode.Strict); + var identifier = "func"; + var args = new[] { "bar", "baz" }; + + var exception = await Should.ThrowAsync(sut.ToJsRuntime().InvokeVoidAsync(identifier, args).AsTask()); + exception.Invocation.Identifier.ShouldBe(identifier); + exception.Invocation.Arguments.ShouldBe(args); + + exception = Should.Throw(() => sut.ToJsRuntime().InvokeAsync(identifier, args)); + exception.Invocation.Identifier.ShouldBe(identifier); + exception.Invocation.Arguments.ShouldBe(args); + } + + [Fact(DisplayName = "Invocations receives before a planned invocation " + + "has result set receives the same result")] + public async Task Test005() + { + var identifier = "func"; + var expectedResult = Guid.NewGuid(); + var sut = new MockJsRuntimeInvokeHandler(JsRuntimeMockMode.Strict); + var plannedInvoke = sut.Setup(identifier); + + var jsRuntime = sut.ToJsRuntime(); + var i1 = jsRuntime.InvokeAsync(identifier); + var i2 = jsRuntime.InvokeAsync(identifier); + + plannedInvoke.SetResult(expectedResult); + + (await i1).ShouldBe(expectedResult); + (await i2).ShouldBe(expectedResult); + } + + [Fact(DisplayName = "Invocations receives after a planned invocation " + + "has result set does not receive the same result as " + + "the invocations before the result was set the first time")] + public async Task Test006() + { + var identifier = "func"; + var sut = new MockJsRuntimeInvokeHandler(JsRuntimeMockMode.Strict); + var plannedInvoke = sut.Setup(identifier); + var jsRuntime = sut.ToJsRuntime(); + + var expectedResult1 = Guid.NewGuid(); + var i1 = jsRuntime.InvokeAsync(identifier); + plannedInvoke.SetResult(expectedResult1); + + var expectedResult2 = Guid.NewGuid(); + var i2 = jsRuntime.InvokeAsync(identifier); + plannedInvoke.SetResult(expectedResult2); + + (await i1).ShouldBe(expectedResult1); + (await i2).ShouldBe(expectedResult2); + } + + [Fact(DisplayName = "A planned invocation can be cancelled for any waiting received invocations.")] + public void Test007() + { + var identifier = "func"; + var sut = new MockJsRuntimeInvokeHandler(JsRuntimeMockMode.Strict); + var plannedInvoke = sut.Setup(identifier); + var invocation = sut.ToJsRuntime().InvokeAsync(identifier); + + plannedInvoke.SetCanceled(); + + invocation.IsCanceled.ShouldBeTrue(); + } + + [Fact(DisplayName = "A planned invocation can throw an exception for any waiting received invocations.")] + public async Task Test008() + { + var identifier = "func"; + var sut = new MockJsRuntimeInvokeHandler(JsRuntimeMockMode.Strict); + var plannedInvoke = sut.Setup(identifier); + var invocation = sut.ToJsRuntime().InvokeAsync(identifier); + var expectedException = new InvalidOperationException("TADA"); + + plannedInvoke.SetException(expectedException); + + var actual = await Should.ThrowAsync(invocation.AsTask()); + actual.ShouldBe(expectedException); + invocation.IsFaulted.ShouldBeTrue(); + } + + [Fact(DisplayName = "Invocations returns all from a planned invocation")] + public void Test009() + { + var identifier = "func"; + var sut = new MockJsRuntimeInvokeHandler(JsRuntimeMockMode.Strict); + var plannedInvoke = sut.Setup(identifier, x => true); + var i1 = sut.ToJsRuntime().InvokeAsync(identifier, "first"); + var i2 = sut.ToJsRuntime().InvokeAsync(identifier, "second"); + + var invocations = plannedInvoke.Invocations; + + invocations.Count.ShouldBe(2); + invocations[0].Arguments[0].ShouldBe("first"); + invocations[1].Arguments[0].ShouldBe("second"); + } + + [Fact(DisplayName = "Arguments used in Setup are matched with invocations")] + public void Test010() + { + var sut = new MockJsRuntimeInvokeHandler(JsRuntimeMockMode.Strict); + var planned = sut.Setup("foo", "bar", 42); + + sut.ToJsRuntime().InvokeAsync("foo", "bar", 42); + + Should.Throw( + () => sut.ToJsRuntime().InvokeAsync("foo", "bar", 41) + ); + + planned.Invocations.Count.ShouldBe(1); + var invocation = planned.Invocations[0]; + invocation.Identifier.ShouldBe("foo"); + invocation.Arguments[0].ShouldBe("bar"); + invocation.Arguments[1].ShouldBe(42); + } + + [Fact(DisplayName = "Argument matcher used in Setup are matched with invocations")] + public void Test011() + { + var sut = new MockJsRuntimeInvokeHandler(JsRuntimeMockMode.Strict); + var planned = sut.Setup("foo", args => args.Count == 1); + + sut.ToJsRuntime().InvokeAsync("foo", 42); + + Should.Throw( + () => sut.ToJsRuntime().InvokeAsync("foo", "bar", 42) + ); - Should.Throw(() => sut.ToJsRuntime().InvokeAsync("ident", new[] { "bar", "baz" })); + planned.Invocations.Count.ShouldBe(1); + var invocation = planned.Invocations[0]; + invocation.Identifier.ShouldBe("foo"); + invocation.Arguments.Count.ShouldBe(1); + invocation.Arguments[0].ShouldBe(42); } - [Fact(DisplayName = "Mock returns task from planned invocation when one is present")] - public async Task Test004() + [Fact(DisplayName = "SetupVoid returns a planned invocation that does not take a result object")] + public async Task Test012() { - var expectedResult = "HELLO WORLD"; - var ident = "fooFunc"; + var identifier = "func"; var sut = new MockJsRuntimeInvokeHandler(JsRuntimeMockMode.Strict); - sut.Setup(ident).SetResult(expectedResult); + var plannedInvoke = sut.SetupVoid(identifier); + + var invocation = sut.ToJsRuntime().InvokeVoidAsync(identifier); + plannedInvoke.SetVoidResult(); + + await invocation; + + invocation.IsCompletedSuccessfully.ShouldBeTrue(); + } + + [Fact(DisplayName = "Arguments used in SetupVoid are matched with invocations")] + public async Task Test013() + { + var sut = new MockJsRuntimeInvokeHandler(JsRuntimeMockMode.Strict); + var planned = sut.SetupVoid("foo", "bar", 42); + + var i1 = sut.ToJsRuntime().InvokeVoidAsync("foo", "bar", 42); + + await Should.ThrowAsync( + sut.ToJsRuntime().InvokeVoidAsync("foo", "bar", 41).AsTask() + ); + + planned.Invocations.Count.ShouldBe(1); + var invocation = planned.Invocations[0]; + invocation.Identifier.ShouldBe("foo"); + invocation.Arguments[0].ShouldBe("bar"); + invocation.Arguments[1].ShouldBe(42); + } + + [Fact(DisplayName = "Argument matcher used in SetupVoid are matched with invocations")] + public async Task Test014() + { + var sut = new MockJsRuntimeInvokeHandler(JsRuntimeMockMode.Strict); + var planned = sut.SetupVoid("foo", args => args.Count == 2); + + var i1 = sut.ToJsRuntime().InvokeVoidAsync("foo", "bar", 42); + + await Should.ThrowAsync( + sut.ToJsRuntime().InvokeVoidAsync("foo", 42).AsTask() + ); - var result = await sut.ToJsRuntime().InvokeAsync(ident, Array.Empty()); + await Should.ThrowAsync( + sut.ToJsRuntime().InvokeVoidAsync("foo").AsTask() + ); - result.ShouldBe(expectedResult); + planned.Invocations.Count.ShouldBe(1); + var invocation = planned.Invocations[0]; + invocation.Identifier.ShouldBe("foo"); + invocation.Arguments.Count.ShouldBe(2); + invocation.Arguments[0].ShouldBe("bar"); + invocation.Arguments[1].ShouldBe(42); } } } From 7411c68743848f31b7f7965f5cc25a111c5423d7 Mon Sep 17 00:00:00 2001 From: Michael J Conrad <32316111+Siphonophora@users.noreply.github.com> Date: Wed, 22 Jan 2020 02:24:35 -0500 Subject: [PATCH 20/30] Add default JsRuntime (#32) * Add default JsRuntime * Update src/Mocking/JSInterop/DefaultJsRuntime.cs Co-Authored-By: Egil Hansen * Response to review. Add custom exception and code cleanup * Remove unneded method * Update src/Mocking/JSInterop/MissingMockJsRuntimeException.cs Co-Authored-By: Egil Hansen * Update src/Mocking/JSInterop/MissingMockJsRuntimeException.cs Co-Authored-By: Egil Hansen * Update src/Mocking/JSInterop/MissingMockJsRuntimeException.cs Co-Authored-By: Egil Hansen * Update src/Mocking/JSInterop/MissingMockJsRuntimeException.cs Co-Authored-By: Egil Hansen * Update src/Mocking/JSInterop/MissingMockJsRuntimeException.cs Co-Authored-By: Egil Hansen * Update src/Mocking/JSInterop/MissingMockJsRuntimeException.cs Co-Authored-By: Egil Hansen Co-authored-by: Egil Hansen --- src/Assembly.cs | 2 +- .../MissingMockJsRuntimeException.cs | 41 +++++++++++++++++++ src/Mocking/JSInterop/PlaceholderJsRuntime.cs | 26 ++++++++++++ src/TestServiceProvider.cs | 9 +++- tests/TestServiceProviderTest.cs | 33 +++++++++++++++ 5 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 src/Mocking/JSInterop/MissingMockJsRuntimeException.cs create mode 100644 src/Mocking/JSInterop/PlaceholderJsRuntime.cs create mode 100644 tests/TestServiceProviderTest.cs diff --git a/src/Assembly.cs b/src/Assembly.cs index 92a57b785..1dcfc7065 100644 --- a/src/Assembly.cs +++ b/src/Assembly.cs @@ -1 +1 @@ -[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Egil.RazorComponents.Testing.Library.Tests")] \ No newline at end of file +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Egil.RazorComponents.Testing.Library.Tests")] diff --git a/src/Mocking/JSInterop/MissingMockJsRuntimeException.cs b/src/Mocking/JSInterop/MissingMockJsRuntimeException.cs new file mode 100644 index 000000000..ef56da290 --- /dev/null +++ b/src/Mocking/JSInterop/MissingMockJsRuntimeException.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; + +namespace Egil.RazorComponents.Testing +{ + /// + /// Exception use to indicate that a MockJsRuntime is required by a test + /// but was not provided. + /// + [SuppressMessage("Design", "CA1032:Implement standard exception constructors", Justification = "")] + public class MissingMockJsRuntimeException : Exception + { + /// + /// Identifer string used in the JSInvoke method. + /// + public string Identifier { get; } + + /// + /// Arguments passed to the JSInvoke method. + /// + public IReadOnlyList Arguments { get; } + + /// + /// Creates a new instance of the + /// with the arguments used in the invocation. + /// + /// The identifer used in the invocation. + /// The args used in the invocation, if any + public MissingMockJsRuntimeException(string identifier, object[] arguments) + : base($"This test requires a IJsRuntime to be supplied, because the component under test invokes the IJsRuntime during the test. The invoked method is '{identifier}' and the invocation arguments are stored in the {nameof(Arguments)} property of this exception. Guidance on mocking the IJsRuntime is available in the testing library's Wiki.") + { + Identifier = identifier; + Arguments = arguments; + HelpLink = "https://github.com/egil/razor-components-testing-library/wiki/Mocking-JsRuntime"; + } + } +} diff --git a/src/Mocking/JSInterop/PlaceholderJsRuntime.cs b/src/Mocking/JSInterop/PlaceholderJsRuntime.cs new file mode 100644 index 000000000..fc8095c32 --- /dev/null +++ b/src/Mocking/JSInterop/PlaceholderJsRuntime.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.JSInterop; + +namespace Egil.RazorComponents.Testing.Mocking.JSInterop +{ + /// + /// This JsRuntime is used to provide users with helpful exceptions if they fail to provide a mock when required. + /// + internal class PlaceholderJsRuntime : IJSRuntime + { + public ValueTask InvokeAsync(string identifier, object[] args) + { + throw new MissingMockJsRuntimeException(identifier, args); + } + + public ValueTask InvokeAsync(string identifier, CancellationToken cancellationToken, object[] args) + { + throw new MissingMockJsRuntimeException(identifier, args); + } + } +} diff --git a/src/TestServiceProvider.cs b/src/TestServiceProvider.cs index 79f15a98d..b97df3901 100644 --- a/src/TestServiceProvider.cs +++ b/src/TestServiceProvider.cs @@ -1,4 +1,6 @@ -using Microsoft.Extensions.DependencyInjection; +using Egil.RazorComponents.Testing.Mocking.JSInterop; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.JSInterop; using System; using System.Diagnostics.CodeAnalysis; @@ -13,6 +15,11 @@ public sealed class TestServiceProvider : IServiceProvider, IDisposable private readonly ServiceCollection _serviceCollection = new ServiceCollection(); private ServiceProvider? _serviceProvider; + public TestServiceProvider() + { + _serviceCollection.AddSingleton(); + } + /// /// Gets whether this has been initialized, and /// no longer will accept calls to the AddService's methods. diff --git a/tests/TestServiceProviderTest.cs b/tests/TestServiceProviderTest.cs new file mode 100644 index 000000000..cc41ad827 --- /dev/null +++ b/tests/TestServiceProviderTest.cs @@ -0,0 +1,33 @@ +using Egil.RazorComponents.Testing.EventDispatchExtensions; +using Egil.RazorComponents.Testing.Extensions; +using Egil.RazorComponents.Testing.Mocking.JSInterop; +using Egil.RazorComponents.Testing.SampleComponents; +using Egil.RazorComponents.Testing.SampleComponents.Data; +using Shouldly; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Xunit; + +namespace Egil.RazorComponents.Testing +{ + public class TestServiceProviderTest : ComponentTestFixture + { + [Fact(DisplayName = "The test service provider should register a placeholder IJSRuntime " + + "which throws exceptions")] + public void Test001() + { + var ex = Assert.Throws(() => RenderComponent()); + Assert.True(ex?.InnerException is MissingMockJsRuntimeException); + } + + [Fact(DisplayName = "The placeholder IJSRuntime is overriden by a supplied mock and does not throw")] + public void Test002() + { + Services.AddMockJsRuntime(); + + RenderComponent(); + } + } +} From 7721421652cb87c4dca7229e4ae3d99f4d607a38 Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Wed, 22 Jan 2020 14:17:50 +0000 Subject: [PATCH 21/30] Add .vscode to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7e2388353..be9a19011 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ bld/ # Visual Studio 2015/2017 cache/options directory .vs/ +.vscode/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ From 6d57f1907a4b7d31007a63ef597c732735d99e52 Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Wed, 22 Jan 2020 15:50:27 +0000 Subject: [PATCH 22/30] TestServiceProvider now explictly implements IServiceCOlelction (#40) * TestServiceProvider now explictly implements IServiceCOlelction * Tweaks to namespaces --- .../Pages/FetchDataTest.razor | 2 +- sample/tests/Tests/Pages/FetchDataTest.cs | 5 +- sample/tests/Tests/Pages/TodosTest.cs | 7 +- sample/tests/_Imports.razor | 3 +- src/Mocking/MockHttpExtensions.cs | 3 +- src/Mocking/MockJsRuntimeExtensions.cs | 3 +- src/TestContext.cs | 6 +- src/TestServiceProvider.cs | 201 +++++++----------- template/template/_Imports.razor | 3 + ...ectImplicitRazorTestContextAvailable.razor | 6 +- tests/RenderedFragmentTest.cs | 3 +- tests/TestServiceProviderTest.cs | 125 ++++++++++- tests/_Imports.razor | 1 + 13 files changed, 218 insertions(+), 150 deletions(-) diff --git a/sample/tests/RazorTestComponents/Pages/FetchDataTest.razor b/sample/tests/RazorTestComponents/Pages/FetchDataTest.razor index 84833cc21..2f2b021e1 100644 --- a/sample/tests/RazorTestComponents/Pages/FetchDataTest.razor +++ b/sample/tests/RazorTestComponents/Pages/FetchDataTest.razor @@ -23,7 +23,7 @@ void Setup() { - Services.AddService(forecastService); + Services.AddSingleton(forecastService); } void InitialLoadingHtmlRendersCorrectly() diff --git a/sample/tests/Tests/Pages/FetchDataTest.cs b/sample/tests/Tests/Pages/FetchDataTest.cs index 99df3d578..1507beb99 100644 --- a/sample/tests/Tests/Pages/FetchDataTest.cs +++ b/sample/tests/Tests/Pages/FetchDataTest.cs @@ -8,6 +8,7 @@ using Xunit; using Egil.RazorComponents.Testing.SampleApp.Pages; using Shouldly; +using Microsoft.Extensions.DependencyInjection; namespace Egil.RazorComponents.Testing.SampleApp.CodeOnlyTests { @@ -17,7 +18,7 @@ public class FetchDataTest : ComponentTestFixture public void Test001() { // Arrange - add the mock forecast service - Services.AddService(); + Services.AddSingleton(); // Act - render the FetchData component var cut = RenderComponent(); @@ -35,7 +36,7 @@ public void Test002() // Setup the mock forecast service var forecasts = new[] { new WeatherForecast { Date = DateTime.Now, Summary = "Testy", TemperatureC = 42 } }; var mockForecastService = new MockForecastService(); - Services.AddService(mockForecastService); + Services.AddSingleton(mockForecastService); // Arrange - render the FetchData component var cut = RenderComponent(); diff --git a/sample/tests/Tests/Pages/TodosTest.cs b/sample/tests/Tests/Pages/TodosTest.cs index 537fcc9b8..663b3afbd 100644 --- a/sample/tests/Tests/Pages/TodosTest.cs +++ b/sample/tests/Tests/Pages/TodosTest.cs @@ -10,6 +10,7 @@ using Egil.RazorComponents.Testing.SampleApp.Data; using Egil.RazorComponents.Testing.EventDispatchExtensions; using Egil.RazorComponents.Testing.SampleApp.Pages; +using Microsoft.Extensions.DependencyInjection; namespace Egil.RazorComponents.Testing.SampleApp.CodeOnlyTests.Pages { @@ -30,7 +31,7 @@ public void Test001() var getTask = new TaskCompletionSource>(); var todoSrv = new Mock(); todoSrv.Setup(x => x.GetAll()).Returns(getTask.Task); - Services.AddService(todoSrv.Object); + Services.AddSingleton(todoSrv.Object); // act var page = RenderComponent(); @@ -51,7 +52,7 @@ public void Test002() var todos = new[] { new Todo { Id = 1, Text = "First" } }; var todoSrv = new Mock(); todoSrv.Setup(x => x.GetAll()).Returns(Task.FromResult>(todos)); - Services.AddService(todoSrv.Object); + Services.AddSingleton(todoSrv.Object); // act var page = RenderComponent(); @@ -66,7 +67,7 @@ public void Test003() { // arrange var todoSrv = new Mock(); - Services.AddService(todoSrv.Object); + Services.AddSingleton(todoSrv.Object); var page = RenderComponent(); // act diff --git a/sample/tests/_Imports.razor b/sample/tests/_Imports.razor index 94b4791dd..3145c5f83 100644 --- a/sample/tests/_Imports.razor +++ b/sample/tests/_Imports.razor @@ -1,16 +1,15 @@ @using Microsoft.AspNetCore.Components.Web +@using Microsoft.Extensions.DependencyInjection @using Egil.RazorComponents.Testing @using Egil.RazorComponents.Testing.EventDispatchExtensions @using Egil.RazorComponents.Testing.Mocking.JSInterop @using Egil.RazorComponents.Testing.Asserting -@using Egil.RazorComponents.Testing.Mocking.JSInterop @using Egil.RazorComponents.Testing.SampleApp @using Egil.RazorComponents.Testing.SampleApp.Data @using Egil.RazorComponents.Testing.SampleApp.Components @using Egil.RazorComponents.Testing.SampleApp.Pages - @using Xunit @using Shouldly diff --git a/src/Mocking/MockHttpExtensions.cs b/src/Mocking/MockHttpExtensions.cs index 79e706b45..598a5f4c6 100644 --- a/src/Mocking/MockHttpExtensions.cs +++ b/src/Mocking/MockHttpExtensions.cs @@ -6,6 +6,7 @@ using System.Text.Json; using System.Net.Http.Headers; using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; namespace Egil.RazorComponents.Testing { @@ -27,7 +28,7 @@ public static MockHttpMessageHandler AddMockHttp(this TestServiceProvider servic var mockHttp = new MockHttpMessageHandler(); var httpClient = mockHttp.ToHttpClient(); httpClient.BaseAddress = new Uri("http://example.com"); - serviceProvider.AddService(httpClient); + serviceProvider.AddSingleton(httpClient); return mockHttp; } diff --git a/src/Mocking/MockJsRuntimeExtensions.cs b/src/Mocking/MockJsRuntimeExtensions.cs index 2c40042f8..153e5b9dd 100644 --- a/src/Mocking/MockJsRuntimeExtensions.cs +++ b/src/Mocking/MockJsRuntimeExtensions.cs @@ -1,5 +1,6 @@ using System; using Egil.RazorComponents.Testing.Mocking.JSInterop; +using Microsoft.Extensions.DependencyInjection; namespace Egil.RazorComponents.Testing { @@ -18,7 +19,7 @@ public static MockJsRuntimeInvokeHandler AddMockJsRuntime(this TestServiceProvid var result = new MockJsRuntimeInvokeHandler(mode); - serviceProvider.AddService(result.ToJsRuntime()); + serviceProvider.AddSingleton(result.ToJsRuntime()); return result; } diff --git a/src/TestContext.cs b/src/TestContext.cs index 4254e88f6..72d8c62b2 100644 --- a/src/TestContext.cs +++ b/src/TestContext.cs @@ -1,10 +1,12 @@ using AngleSharp.Dom; using Egil.RazorComponents.Testing.Diffing; +using Egil.RazorComponents.Testing.Mocking.JSInterop; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.RenderTree; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.JSInterop; using System; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -32,9 +34,11 @@ public class TestContext : ITestContext, IDisposable /// public TestContext() { + Services.AddSingleton(new PlaceholderJsRuntime()); + _renderer = new Lazy(() => { - var loggerFactory = Services.GetService() ?? new NullLoggerFactory(); + var loggerFactory = Services.GetService() ?? NullLoggerFactory.Instance; return new TestRenderer(Services, loggerFactory); }); _htmlParser = new Lazy(() => diff --git a/src/TestServiceProvider.cs b/src/TestServiceProvider.cs index b97df3901..14c0a5419 100644 --- a/src/TestServiceProvider.cs +++ b/src/TestServiceProvider.cs @@ -1,8 +1,8 @@ -using Egil.RazorComponents.Testing.Mocking.JSInterop; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.JSInterop; -using System; +using System; +using System.Collections; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; namespace Egil.RazorComponents.Testing { @@ -10,15 +10,16 @@ namespace Egil.RazorComponents.Testing /// Represents a and /// as a single type used for test purposes. /// - public sealed class TestServiceProvider : IServiceProvider, IDisposable + [SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix")] + public sealed class TestServiceProvider : IServiceProvider, IServiceCollection, IDisposable { - private readonly ServiceCollection _serviceCollection = new ServiceCollection(); + private readonly IServiceCollection _serviceCollection; private ServiceProvider? _serviceProvider; - public TestServiceProvider() - { - _serviceCollection.AddSingleton(); - } + /// + /// Gets a reusable default test service provider. + /// + public static readonly IServiceProvider Default = new TestServiceProvider(new ServiceCollection(), true); /// /// Gets whether this has been initialized, and @@ -26,164 +27,104 @@ public TestServiceProvider() /// public bool IsProviderInitialized => _serviceProvider is { }; - /// - /// Adds a singleton service of the type specified in TService with an implementation - /// type specified in TImplementation using the factory specified in implementationFactory - /// to this . - /// - /// The type of the service to add. - /// The type of the implementation to use. - /// The factory that creates the service. - /// A reference to this instance after the operation has completed. - public TestServiceProvider AddService(Func implementationFactory) - where TService : class - where TImplementation : class, TService - { - CheckInitializedAndThrow(); - _serviceCollection.AddSingleton(implementationFactory); - return this; - } + /// + public int Count => _serviceCollection.Count; - /// - /// Adds a singleton service of the type specified in with a factory specified - /// in to this . - /// - /// The type of the service to add. - /// The factory that creates the service. - /// A reference to this instance after the operation has completed. - public TestServiceProvider AddService(Func implementationFactory) where TService : class - { - CheckInitializedAndThrow(); - _serviceCollection.AddSingleton(implementationFactory); - return this; - } + /// + public bool IsReadOnly => IsProviderInitialized || _serviceCollection.IsReadOnly; - /// - /// Adds a singleton service of the type specified in to this . - /// - /// The type of the service to add. - /// A reference to this instance after the operation has completed. - public TestServiceProvider AddService() where TService : class + /// + public ServiceDescriptor this[int index] { - CheckInitializedAndThrow(); - _serviceCollection.AddSingleton(); - return this; + get => _serviceCollection[index]; + set + { + CheckInitializedAndThrow(); + _serviceCollection[index] = value; + } } - /// - /// Adds a singleton service of the type specified in to this . - /// - /// The type of the service to register and the implementation to use. - /// A reference to this instance after the operation has completed. - public TestServiceProvider AddService(Type serviceType) + public TestServiceProvider(IServiceCollection? initialServiceCollection = null) : this(initialServiceCollection ?? new ServiceCollection(), false) { - CheckInitializedAndThrow(); - _serviceCollection.AddSingleton(serviceType); - return this; } - /// - /// Adds a singleton service of the type specified in with an implementation - /// type specified in to this . - /// - /// The type of the service to add. - /// The type of the implementation to use. - /// A reference to this instance after the operation has completed. - public TestServiceProvider AddService() - where TService : class - where TImplementation : class, TService + private TestServiceProvider(IServiceCollection initialServiceCollection, bool initializeProvider) { - CheckInitializedAndThrow(); - _serviceCollection.AddSingleton(); - return this; + _serviceCollection = initialServiceCollection; + if (initializeProvider) _serviceProvider = _serviceCollection.BuildServiceProvider(); } /// - /// Adds a singleton service of the type specified in with a factory - /// specified in to this . + /// Get service of type T from the test provider. /// - /// The type of the service to register. - /// The factory that creates the service. - /// - public TestServiceProvider AddService(Type serviceType, Func implementationFactory) + /// The type of service object to get. + /// A service object of type T or null if there is no such service. + public TService GetService() => (TService)GetService(typeof(TService)); + + /// + public object GetService(Type serviceType) { - CheckInitializedAndThrow(); - _serviceCollection.AddSingleton(serviceType, implementationFactory); - return this; + if (_serviceProvider is null) + _serviceProvider = _serviceCollection.BuildServiceProvider(); + + return _serviceProvider.GetService(serviceType); } - /// - /// Adds a singleton service of the type specified in with an implementation - /// of the type specified in to this . - /// - /// The type of the service to register. - /// The implementation type of the service. - /// A reference to this instance after the operation has completed. - public TestServiceProvider AddService(Type serviceType, Type implementationType) + /// + public IEnumerator GetEnumerator() => _serviceCollection.GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + public void Dispose() { - CheckInitializedAndThrow(); - _serviceCollection.AddSingleton(serviceType, implementationType); - return this; + _serviceProvider?.Dispose(); } - /// - /// Adds a singleton service of the type specified in with an instance specified in - /// to this . - /// - /// The instance of the service. - /// A reference to this instance after the operation has completed. - public TestServiceProvider AddService(TService implementationInstance) where TService : class + /// + public int IndexOf(ServiceDescriptor item) => _serviceCollection.IndexOf(item); + /// + public void Insert(int index, ServiceDescriptor item) { CheckInitializedAndThrow(); - _serviceCollection.AddSingleton(implementationInstance); - return this; + _serviceCollection.Insert(index, item); } - - /// - /// Adds a singleton service of the type specified in with an instance specified in - /// to this . - /// - /// The type of the service to register. - /// The instance of the service. - /// A reference to this instance after the operation has completed. - public TestServiceProvider AddService(Type serviceType, object implementationInstance) + /// + public void RemoveAt(int index) { CheckInitializedAndThrow(); - _serviceCollection.AddSingleton(serviceType, implementationInstance); - return this; + _serviceCollection.RemoveAt(index); } - /// - /// Get service of type T from the test provider. - /// - /// The type of service object to get. - /// A service object of type T or null if there is no such service. - public TService GetService() + /// + public void Add(ServiceDescriptor item) { - if (_serviceProvider is null) - _serviceProvider = _serviceCollection.BuildServiceProvider(); - return _serviceProvider.GetService(); + CheckInitializedAndThrow(); + _serviceCollection.Add(item); } - /// - public object GetService(Type serviceType) + public void Clear() { - if (_serviceProvider is null) - _serviceProvider = _serviceCollection.BuildServiceProvider(); - - return _serviceProvider.GetService(serviceType); + CheckInitializedAndThrow(); + _serviceCollection.Clear(); } /// - public void Dispose() + public bool Contains(ServiceDescriptor item) => _serviceCollection.Contains(item); + /// + public void CopyTo(ServiceDescriptor[] array, int arrayIndex) => _serviceCollection.CopyTo(array, arrayIndex); + /// + public bool Remove(ServiceDescriptor item) { - _serviceProvider?.Dispose(); + CheckInitializedAndThrow(); + return _serviceCollection.Remove(item); } private void CheckInitializedAndThrow() { if (IsProviderInitialized) - throw new InvalidOperationException("New services cannot be added to provider after it has been initialized."); + throw new InvalidOperationException("Services cannot be added to provider after it has been initialized."); } } -} +} \ No newline at end of file diff --git a/template/template/_Imports.razor b/template/template/_Imports.razor index 6b095ce66..d248d537f 100644 --- a/template/template/_Imports.razor +++ b/template/template/_Imports.razor @@ -1,7 +1,10 @@ @using Microsoft.AspNetCore.Components.Web +@using Microsoft.Extensions.DependencyInjection + @using Egil.RazorComponents.Testing @using Egil.RazorComponents.Testing.Diffing @using Egil.RazorComponents.Testing.Asserting @using Egil.RazorComponents.Testing.EventDispatchExtensions @using Egil.RazorComponents.Testing.Mocking.JSInterop + @using Xunit \ No newline at end of file diff --git a/tests/Components/TestComponentBaseTest/CorrectImplicitRazorTestContextAvailable.razor b/tests/Components/TestComponentBaseTest/CorrectImplicitRazorTestContextAvailable.razor index 36092886e..f5fdad84c 100644 --- a/tests/Components/TestComponentBaseTest/CorrectImplicitRazorTestContextAvailable.razor +++ b/tests/Components/TestComponentBaseTest/CorrectImplicitRazorTestContextAvailable.razor @@ -14,7 +14,7 @@ void Setup1() { - Services.AddService(dep1Expected); + Services.AddSingleton(dep1Expected); } void Test1() @@ -52,7 +52,7 @@ void Setup2() { - Services.AddService(dep2Expected); + Services.AddSingleton(dep2Expected); } void Test2() @@ -86,7 +86,7 @@ } } - +

@nameof(Dep3)

diff --git a/tests/RenderedFragmentTest.cs b/tests/RenderedFragmentTest.cs index ae5474a8f..ac8075e5e 100644 --- a/tests/RenderedFragmentTest.cs +++ b/tests/RenderedFragmentTest.cs @@ -1,6 +1,7 @@ using Egil.RazorComponents.Testing.Extensions; using Egil.RazorComponents.Testing.SampleComponents; using Egil.RazorComponents.Testing.SampleComponents.Data; +using Microsoft.Extensions.DependencyInjection; using Shouldly; using System; using System.Collections.Generic; @@ -32,7 +33,7 @@ public void Test002() public void Test003() { var testData = new AsyncNameDep(); - Services.AddService(testData); + Services.AddSingleton(testData); var cut = RenderComponent(); var initialValue = cut.Nodes.Find("p").OuterHtml; diff --git a/tests/TestServiceProviderTest.cs b/tests/TestServiceProviderTest.cs index cc41ad827..f16208f9d 100644 --- a/tests/TestServiceProviderTest.cs +++ b/tests/TestServiceProviderTest.cs @@ -1,10 +1,13 @@ -using Egil.RazorComponents.Testing.EventDispatchExtensions; +using Egil.RazorComponents.Testing; +using Egil.RazorComponents.Testing.EventDispatchExtensions; using Egil.RazorComponents.Testing.Extensions; using Egil.RazorComponents.Testing.Mocking.JSInterop; using Egil.RazorComponents.Testing.SampleComponents; using Egil.RazorComponents.Testing.SampleComponents.Data; +using Microsoft.Extensions.DependencyInjection; using Shouldly; using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; @@ -14,16 +17,128 @@ namespace Egil.RazorComponents.Testing { public class TestServiceProviderTest : ComponentTestFixture { - [Fact(DisplayName = "The test service provider should register a placeholder IJSRuntime " + - "which throws exceptions")] + class DummyService { } + class AnotherDummyService { } + class OneMoreDummyService { } + + [Fact(DisplayName = "Provider initialized without a service collection has zero services by default")] public void Test001() + { + using var sut = new TestServiceProvider(); + + sut.Count.ShouldBe(0); + } + + [Fact(DisplayName = "Provider initialized with a service collection has the services form the provided collection")] + public void Test002() + { + var services = new ServiceCollection(); + services.AddSingleton(new DummyService()); + using var sut = new TestServiceProvider(services); + + sut.Count.ShouldBe(1); + sut[0].ServiceType.ShouldBe(typeof(DummyService)); + } + + [Fact(DisplayName = "Services can be registered in the provider like a normal service collection")] + public void Test010() + { + using var sut = new TestServiceProvider(); + + sut.Add(new ServiceDescriptor(typeof(DummyService), new DummyService())); + sut.Insert(0, new ServiceDescriptor(typeof(AnotherDummyService), new AnotherDummyService())); + sut[1] = new ServiceDescriptor(typeof(DummyService), new DummyService()); + + sut.Count.ShouldBe(2); + sut[0].ServiceType.ShouldBe(typeof(AnotherDummyService)); + sut[1].ServiceType.ShouldBe(typeof(DummyService)); + } + + [Fact(DisplayName = "Services can be removed in the provider like a normal service collection")] + public void Test011() + { + using var sut = new TestServiceProvider(); + var descriptor = new ServiceDescriptor(typeof(DummyService), new DummyService()); + var anotherDescriptor = new ServiceDescriptor(typeof(AnotherDummyService), new AnotherDummyService()); + var oneMoreDescriptor = new ServiceDescriptor(typeof(OneMoreDummyService), new OneMoreDummyService()); + + sut.Add(descriptor); + sut.Add(anotherDescriptor); + sut.Add(oneMoreDescriptor); + + sut.Remove(descriptor); + sut.Count.ShouldBe(2); + + sut.RemoveAt(1); + sut.Count.ShouldBe(1); + + sut.Clear(); + sut.ShouldBeEmpty(); + } + + [Fact(DisplayName = "Misc collection methods works as expected")] + public void Test012() + { + using var sut = new TestServiceProvider(); + var descriptor = new ServiceDescriptor(typeof(DummyService), new DummyService()); + var copyToTarget = new ServiceDescriptor[1]; + sut.Add(descriptor); + + sut.IndexOf(descriptor).ShouldBe(0); + sut.Contains(descriptor).ShouldBeTrue(); + sut.CopyTo(copyToTarget, 0); + copyToTarget[0].ShouldBe(descriptor); + sut.IsReadOnly.ShouldBeFalse(); + ((IEnumerable)sut).OfType().Count().ShouldBe(1); + } + + [Fact(DisplayName = "After the first service is requested, " + + "the provider does not allow changes to service collection")] + public void Test013() + { + var descriptor = new ServiceDescriptor(typeof(AnotherDummyService), new AnotherDummyService()); + + using var sut = new TestServiceProvider(); + sut.AddSingleton(new DummyService()); + sut.GetService(); + + // Try adding + Should.Throw(() => sut.Add(descriptor)); + Should.Throw(() => sut.Insert(0, descriptor)); + Should.Throw(() => sut[0] = descriptor); + + // Try removing + Should.Throw(() => sut.Remove(descriptor)); + Should.Throw(() => sut.RemoveAt(0)); + Should.Throw(() => sut.Clear()); + + // Verify state + sut.IsProviderInitialized.ShouldBeTrue(); + sut.IsReadOnly.ShouldBeTrue(); + } + + [Fact(DisplayName = "Registered services can be retrieved from the provider")] + public void Test020() + { + using var sut = new TestServiceProvider(); + var expected = new DummyService(); + sut.AddSingleton(expected); + + var actual = sut.GetService(); + + actual.ShouldBe(expected); + } + + [Fact(DisplayName = "The test service provider should register a placeholder IJSRuntime " + + "which throws exceptions")] + public void Test021() { var ex = Assert.Throws(() => RenderComponent()); - Assert.True(ex?.InnerException is MissingMockJsRuntimeException); + ex.InnerException.ShouldBeOfType(); } [Fact(DisplayName = "The placeholder IJSRuntime is overriden by a supplied mock and does not throw")] - public void Test002() + public void Test022() { Services.AddMockJsRuntime(); diff --git a/tests/_Imports.razor b/tests/_Imports.razor index a86dfc32b..94baed4ea 100644 --- a/tests/_Imports.razor +++ b/tests/_Imports.razor @@ -1,4 +1,5 @@ @using Microsoft.AspNetCore.Components.Web +@using Microsoft.Extensions.DependencyInjection @using Egil.RazorComponents.Testing.SampleComponents @using Egil.RazorComponents.Testing.SampleComponents.Data @using Shouldly From ea034d13d864a779c226c6ca1ec022846a27e3c8 Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Wed, 22 Jan 2020 21:25:04 +0000 Subject: [PATCH 23/30] Added async setup method to snapshot test --- src/Components/Fixture.cs | 12 ++++-------- src/Components/FragmentBase.cs | 3 +++ src/Components/SnapshotTest.cs | 11 ++++++++--- src/Components/TestComponentBase.cs | 5 +++-- .../TestComponentBaseTest/SnapshotTestTest.razor | 13 +++++++++++++ 5 files changed, 31 insertions(+), 13 deletions(-) create mode 100644 tests/Components/TestComponentBaseTest/SnapshotTestTest.razor diff --git a/src/Components/Fixture.cs b/src/Components/Fixture.cs index 4785235b6..3a052007e 100644 --- a/src/Components/Fixture.cs +++ b/src/Components/Fixture.cs @@ -13,9 +13,9 @@ namespace Egil.RazorComponents.Testing public class Fixture : FragmentBase { private Action _setup = NoopTestMethod; - private Func _setupAsync = NoopAsyncTestMethod; + private Func _setupAsync = NoopTestMethodAsync; private Action _test = NoopTestMethod; - private Func _testAsync = NoopAsyncTestMethod; + private Func _testAsync = NoopTestMethodAsync; private IReadOnlyCollection _tests = Array.Empty(); private IReadOnlyCollection> _testsAsync = Array.Empty>(); @@ -34,7 +34,7 @@ public class Fixture : FragmentBase /// Gets or sets the asynchronous setup action to perform before the action, /// action and and actions are invoked. /// - [Parameter] public Func SetupAsync { get => _setupAsync; set => _setupAsync = value ?? NoopAsyncTestMethod; } + [Parameter] public Func SetupAsync { get => _setupAsync; set => _setupAsync = value ?? NoopTestMethodAsync; } /// /// Gets or sets the first test action to invoke, after the action has @@ -52,7 +52,7 @@ public class Fixture : FragmentBase /// Use this to assert against the and 's /// defined in the . /// - [Parameter] public Func TestAsync { get => _testAsync; set => _testAsync = value ?? NoopAsyncTestMethod; } + [Parameter] public Func TestAsync { get => _testAsync; set => _testAsync = value ?? NoopTestMethodAsync; } /// /// Gets or sets the test actions to invoke, one at the time, in the order they are placed @@ -73,9 +73,5 @@ public class Fixture : FragmentBase /// defined in the . /// [Parameter] public IReadOnlyCollection> TestsAsync { get => _testsAsync; set => _testsAsync = value ?? Array.Empty>(); } - - private static void NoopTestMethod() { } - - private static Task NoopAsyncTestMethod() => Task.CompletedTask; } } diff --git a/src/Components/FragmentBase.cs b/src/Components/FragmentBase.cs index 73f0897f7..a57b91c10 100644 --- a/src/Components/FragmentBase.cs +++ b/src/Components/FragmentBase.cs @@ -9,6 +9,9 @@ namespace Egil.RazorComponents.Testing /// public abstract class FragmentBase : IComponent { + internal static void NoopTestMethod() { } + internal static Task NoopTestMethodAsync() => Task.CompletedTask; + /// /// Gets or sets the child content of the fragment. /// diff --git a/src/Components/SnapshotTest.cs b/src/Components/SnapshotTest.cs index f5cffdb5f..224139fff 100644 --- a/src/Components/SnapshotTest.cs +++ b/src/Components/SnapshotTest.cs @@ -16,6 +16,7 @@ namespace Egil.RazorComponents.Testing public class SnapshotTest : FragmentBase { private Action _setup = NoopTestMethod; + private Func _setupAsync = NoopTestMethodAsync; /// /// A description or name for the test that will be displayed if the test fails. @@ -23,12 +24,16 @@ public class SnapshotTest : FragmentBase [Parameter] public string? Description { get; set; } /// - /// A method to be called component and component - /// is rendered. Use to e.g. setup services that the test input needs to render. + /// Gets or sets the setup action to perform before the and + /// is rendered and compared. /// [Parameter] public Action Setup { get => _setup; set => _setup = value ?? NoopTestMethod; } - private static void NoopTestMethod() { } + /// + /// Gets or sets the setup action to perform before the and + /// is rendered and compared. + /// + [Parameter] public Func SetupAsync { get => _setupAsync; set => _setupAsync = value ?? NoopTestMethodAsync; } } /// diff --git a/src/Components/TestComponentBase.cs b/src/Components/TestComponentBase.cs index 5c1a41bd8..66d57cbd7 100644 --- a/src/Components/TestComponentBase.cs +++ b/src/Components/TestComponentBase.cs @@ -56,7 +56,7 @@ public async Task RazorTest() container.Render(BuildRenderTree); await ExecuteFixtureTests(container).ConfigureAwait(false); - ExecuteSnapshotTests(container); + await ExecuteSnapshotTests(container).ConfigureAwait(false); } /// @@ -148,7 +148,7 @@ private static async Task InvokeFixtureAction(Fixture fixture, Func action } } - private void ExecuteSnapshotTests(ContainerComponent container) + private async Task ExecuteSnapshotTests(ContainerComponent container) { foreach (var (_, snapshot) in container.GetComponents()) { @@ -157,6 +157,7 @@ private void ExecuteSnapshotTests(ContainerComponent container) var context = _testContextAdapter.ActivateSnapshotTestContext(testData); snapshot.Setup(); + await snapshot.SetupAsync().ConfigureAwait(false); var actual = context.RenderTestInput(); var expected = context.RenderExpectedOutput(); actual.MarkupMatches(expected, snapshot.Description); diff --git a/tests/Components/TestComponentBaseTest/SnapshotTestTest.razor b/tests/Components/TestComponentBaseTest/SnapshotTestTest.razor new file mode 100644 index 000000000..b0e79cace --- /dev/null +++ b/tests/Components/TestComponentBaseTest/SnapshotTestTest.razor @@ -0,0 +1,13 @@ +@inherits TestComponentBase +@using Shouldly + + + + + + + + @inject ITestDep testDep +

@testDep.Name

+
+
\ No newline at end of file From 893fdad8489bc826a7ac3e26b6ad3000b37b5ca2 Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Wed, 22 Jan 2020 21:35:28 +0000 Subject: [PATCH 24/30] Added test of SnapshotTests use of setup methods --- src/TestServiceProvider.cs | 5 +++++ .../TestComponentBaseTest/SnapshotTestTest.razor | 12 +++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/TestServiceProvider.cs b/src/TestServiceProvider.cs index 14c0a5419..6f46f29c9 100644 --- a/src/TestServiceProvider.cs +++ b/src/TestServiceProvider.cs @@ -44,6 +44,11 @@ public ServiceDescriptor this[int index] } } + /// + /// Creates an instance of the and sets its service collection to the + /// provided , if any. + /// + /// public TestServiceProvider(IServiceCollection? initialServiceCollection = null) : this(initialServiceCollection ?? new ServiceCollection(), false) { } diff --git a/tests/Components/TestComponentBaseTest/SnapshotTestTest.razor b/tests/Components/TestComponentBaseTest/SnapshotTestTest.razor index b0e79cace..46049852e 100644 --- a/tests/Components/TestComponentBaseTest/SnapshotTestTest.razor +++ b/tests/Components/TestComponentBaseTest/SnapshotTestTest.razor @@ -1,13 +1,19 @@ @inherits TestComponentBase @using Shouldly - +@code { + class Dep1 : ITestDep { public string Name { get; } = "FOO"; } + class Dep2 : IAsyncTestDep { public Task GetData() => Task.FromResult("BAR"); } +} + + - @inject ITestDep testDep -

@testDep.Name

+

FOO

+

BAR

\ No newline at end of file From 865ea40e0d29641c9007e2cec03a555fd23c41aa Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Wed, 22 Jan 2020 21:53:57 +0000 Subject: [PATCH 25/30] Update to readme --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 429952ff6..a91aee89f 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,10 @@ This library's goal is to make it easy to write _comprehensive, stable unit test - [Mocking JsRuntime](https://github.com/egil/razor-components-testing-library/wiki/Mocking-JsRuntime) - [References](https://github.com/egil/razor-components-testing-library/wiki/References) - [Contribute](https://github.com/egil/razor-components-testing-library/wiki/Contribute) + +## Contributors + +Shout outs and a big thank you to the contributors to this library. Here they are, in alphabetically: + +- [Michael J Conrad (@Siphonophora)](https://github.com/Siphonophora) +- [Rastislav Novotný (@duracellko)](https://github.com/duracellko) \ No newline at end of file From d246f736559a74ca07554f3b4df7f20d8f4044aa Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Wed, 22 Jan 2020 22:14:54 +0000 Subject: [PATCH 26/30] Removed duplicated MarkupMatches method --- .../MarkupMatchesAssertExtensions.cs | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/src/Asserting/MarkupMatchesAssertExtensions.cs b/src/Asserting/MarkupMatchesAssertExtensions.cs index 80c661c1d..5f55c510a 100644 --- a/src/Asserting/MarkupMatchesAssertExtensions.cs +++ b/src/Asserting/MarkupMatchesAssertExtensions.cs @@ -106,32 +106,6 @@ public static void MarkupMatches(this INode actual, string expected, string? use MarkupMatches(actual, expectedNodes, userMessage); } - /// - /// Verifies that the matches - /// the markup, using the - /// type. - /// - /// Thrown when the markup does not match the markup. - /// The node to verify. - /// The expected markup. - /// A custom user message to display in case the verification fails. - public static void MarkupMatches(this INode actual, string expected, string? userMessage = null) - { - if (actual is null) throw new ArgumentNullException(nameof(actual)); - - INodeList expectedNodes; - if (actual.GetHtmlParser() is { } parser) - { - expectedNodes = parser.Parse(expected); - } - else - { - using var newParser = new TestHtmlParser(); - expectedNodes = newParser.Parse(expected); - } - MarkupMatches(actual, expectedNodes, userMessage); - } - /// /// Verifies that the matches /// the markup, using the From 64faa760892b9605aaf86445331aea0d13b6a90f Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Wed, 22 Jan 2020 22:16:32 +0000 Subject: [PATCH 27/30] Removed PR trigger --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 167da5751..45998aa7a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,7 +1,7 @@ name: CI -on: [push, pull_request] +on: push env: VERSION: 1337.0.0 From 3e4e3c86ca81899958b90df057eefb00c3afed70 Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Thu, 23 Jan 2020 10:28:51 +0000 Subject: [PATCH 28/30] new version of diffing --- src/Egil.RazorComponents.Testing.Library.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Egil.RazorComponents.Testing.Library.csproj b/src/Egil.RazorComponents.Testing.Library.csproj index d2c89d0b3..ca14270b8 100644 --- a/src/Egil.RazorComponents.Testing.Library.csproj +++ b/src/Egil.RazorComponents.Testing.Library.csproj @@ -47,7 +47,7 @@ This library's goal is to make it easy to write comprehensive, stable unit tests - + From cc4c71f1cb92ab2cf90e7b46f6270403314a82ee Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Thu, 23 Jan 2020 22:42:00 +0000 Subject: [PATCH 29/30] Fix for issue #43 --- src/Components/ContainerComponent.cs | 48 +++++++++++++-------- src/ElementNotFoundException.cs | 2 +- src/Rendering/IRenderedFragment.cs | 1 + src/Rendering/RenderedFragmentBase.cs | 21 +++++---- tests/RenderComponentTest.cs | 19 +------- tests/RenderedFragmentTest.cs | 50 +++++++++++++++++++++- tests/SampleComponents/RenderOnClick.razor | 9 ++++ 7 files changed, 104 insertions(+), 46 deletions(-) create mode 100644 tests/SampleComponents/RenderOnClick.razor diff --git a/src/Components/ContainerComponent.cs b/src/Components/ContainerComponent.cs index 6a9092469..7a789580f 100644 --- a/src/Components/ContainerComponent.cs +++ b/src/Components/ContainerComponent.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading.Tasks; using Egil.RazorComponents.Testing.Extensions; +using AngleSharp.Css.Dom; namespace Egil.RazorComponents.Testing { @@ -32,7 +33,7 @@ public ContainerComponent(TestRenderer renderer) { if (renderer is null) throw new ArgumentNullException(nameof(renderer)); _renderer = renderer; - ComponentId = _renderer.AttachTestRootComponent(this); + ComponentId = _renderer.AttachTestRootComponent(this); } /// @@ -58,17 +59,14 @@ public void Render(RenderFragment renderFragment) /// component is found, its child content is also searched recursively. /// /// The type of component to find - /// When there are more than one component of type found or if none are found. + /// When a component of type was not found. public (int Id, TComponent Component) GetComponent() where TComponent : IComponent { - var result = GetComponents(); - - if (result.Count == 1) - return result[0]; - else if (result.Count == 0) - throw new InvalidOperationException($"No components of type {typeof(TComponent)} were found in the render tree."); + var result = GetComponent(ComponentId); + if (result.HasValue) + return result.Value; else - throw new InvalidOperationException($"More than one component of type {typeof(TComponent)} was found in the render tree."); + throw new InvalidOperationException($"No components of type {typeof(TComponent)} were found in the render tree."); } /// @@ -84,7 +82,7 @@ public void Render(RenderFragment renderFragment) var ownFrames = _renderer.GetCurrentRenderTreeFrames(componentId); if (ownFrames.Count == 0) { - throw new InvalidOperationException($"{nameof(ContainerComponent)} hasn't yet rendered"); + return Array.Empty<(int Id, TComponent Component)>(); } var result = new List<(int Id, TComponent Component)>(); @@ -97,20 +95,32 @@ public void Render(RenderFragment renderFragment) { result.Add((frame.ComponentId, component)); } - else if (frame.Component.IsCascadingValueComponent()) + result.AddRange(GetComponents(frame.ComponentId)); + } + } + + return result; + } + + private (int Id, TComponent Component)? GetComponent(int componentId) where TComponent : IComponent + { + var ownFrames = _renderer.GetCurrentRenderTreeFrames(componentId); + + for (int i = 0; i < ownFrames.Count; i++) + { + ref var frame = ref ownFrames.Array[i]; + if (frame.FrameType == RenderTreeFrameType.Component) + { + if (frame.Component is TComponent component) { - // It seems as if CascadingValue components works a little different - // than regular components with child content is not rendered - // and available via GetCurrentRenderTreeFrames for the componentId - // of the component that had the CascadingValue as a child. - // Thus we call GetComponents recursively with the CascadingValue's - // componentId to see if the TComponent is inside it. - result.AddRange(GetComponents(frame.ComponentId)); + return (frame.ComponentId, component); } + var result = GetComponent(frame.ComponentId); + if (result != null) return result; } } - return result; + return null; } } } diff --git a/src/ElementNotFoundException.cs b/src/ElementNotFoundException.cs index 1a3caf9fe..a4792322c 100644 --- a/src/ElementNotFoundException.cs +++ b/src/ElementNotFoundException.cs @@ -5,7 +5,7 @@ using System.Text; using System.Threading.Tasks; -namespace Egil.RazorComponents.Testing +namespace Xunit.Sdk { /// /// Represents a failure to find an element in the searched target diff --git a/src/Rendering/IRenderedFragment.cs b/src/Rendering/IRenderedFragment.cs index 9742bb918..7ddc8af4c 100644 --- a/src/Rendering/IRenderedFragment.cs +++ b/src/Rendering/IRenderedFragment.cs @@ -3,6 +3,7 @@ using System.Linq; using AngleSharp.Diffing.Core; using AngleSharp.Dom; +using Xunit.Sdk; namespace Egil.RazorComponents.Testing { diff --git a/src/Rendering/RenderedFragmentBase.cs b/src/Rendering/RenderedFragmentBase.cs index e02050b5b..8801b371b 100644 --- a/src/Rendering/RenderedFragmentBase.cs +++ b/src/Rendering/RenderedFragmentBase.cs @@ -93,7 +93,6 @@ public IReadOnlyList GetChangesSinceSnapshot() return Nodes.CompareTo(_snapshotNodes); } - /// public IReadOnlyList GetChangesSinceFirstRender() { @@ -104,24 +103,30 @@ public IReadOnlyList GetChangesSinceFirstRender() private void ComponentMarkupChanged(in RenderBatch renderBatch) { - if (renderBatch.HasUpdatesTo(ComponentId) || HasChildComponentUpdated(renderBatch)) + if (renderBatch.HasUpdatesTo(ComponentId) || HasChildComponentUpdated(renderBatch, ComponentId)) { ResetLatestRenderCache(); } } - private bool HasChildComponentUpdated(in RenderBatch renderBatch) + private bool HasChildComponentUpdated(in RenderBatch renderBatch, int componentId) { - var frames = TestContext.Renderer.GetCurrentRenderTreeFrames(ComponentId); + var frames = TestContext.Renderer.GetCurrentRenderTreeFrames(componentId); + for (int i = 0; i < frames.Count; i++) { var frame = frames.Array[i]; - - if (renderBatch.HasUpdatesTo(frame.ComponentId)) + if (frame.FrameType == RenderTreeFrameType.Component) { - return true; + if (renderBatch.HasUpdatesTo(frame.ComponentId)) + { + return true; + } + if (HasChildComponentUpdated(in renderBatch, frame.ComponentId)) + { + return true; + } } - } return false; } diff --git a/tests/RenderComponentTest.cs b/tests/RenderComponentTest.cs index 33379530d..e9bd2cb64 100644 --- a/tests/RenderComponentTest.cs +++ b/tests/RenderComponentTest.cs @@ -22,7 +22,7 @@ public void Test003() [Fact(DisplayName = "Nodes should return new instance " + "when a SetParametersAndRender has caused changes to DOM tree")] - public void Tets004() + public void Test004() { var cut = RenderComponent(ChildContent("
")); var initialNodes = cut.Nodes; @@ -35,7 +35,7 @@ public void Tets004() [Fact(DisplayName = "Nodes should return new instance " + "when a Render has caused changes to DOM tree")] - public void Tets005() + public void Test005() { var cut = RenderComponent(); var initialNodes = cut.Nodes; @@ -44,20 +44,5 @@ public void Tets005() Assert.NotSame(initialNodes, cut.Nodes); } - - [Fact(DisplayName = "Nodes should return new instance " + - "when a event handler trigger has caused changes to DOM tree")] - public void Tets006() - { - var cut = RenderComponent(); - var initialNodes = cut.Nodes; - - cut.Find("button").Click(); - - Assert.NotSame(initialNodes, cut.Nodes); - } - - } - } diff --git a/tests/RenderedFragmentTest.cs b/tests/RenderedFragmentTest.cs index ac8075e5e..2ca8f452e 100644 --- a/tests/RenderedFragmentTest.cs +++ b/tests/RenderedFragmentTest.cs @@ -1,6 +1,8 @@ -using Egil.RazorComponents.Testing.Extensions; +using Egil.RazorComponents.Testing.EventDispatchExtensions; +using Egil.RazorComponents.Testing.Extensions; using Egil.RazorComponents.Testing.SampleComponents; using Egil.RazorComponents.Testing.SampleComponents.Data; +using Microsoft.AspNetCore.Components; using Microsoft.Extensions.DependencyInjection; using Shouldly; using System; @@ -8,6 +10,7 @@ using System.Linq; using System.Text; using Xunit; +using Xunit.Sdk; namespace Egil.RazorComponents.Testing { @@ -70,6 +73,51 @@ public void Test005() Assert.NotSame(initialValue, cut.Nodes); } + + + [Fact(DisplayName = "Nodes should return new instance " + + "when a event handler trigger has caused changes to DOM tree")] + public void Test006() + { + var cut = RenderComponent(); + var initialNodes = cut.Nodes; + + cut.Find("button").Click(); + + Assert.NotSame(initialNodes, cut.Nodes); + } + + [Fact(DisplayName = "Nodes should return new instance " + + "when a nested component has caused the DOM tree to change")] + public void Test007() + { + var cut = RenderComponent( + ChildContent>( + ("Value", "FOO"), + ChildContent() + ) + ); + var initialNodes = cut.Nodes; + + cut.Find("button").Click(); + + Assert.NotSame(initialNodes, cut.Nodes); + } + + [Fact(DisplayName = "Nodes should return the same instance " + + "when a re-render does not causes the DOM to change")] + public void Test008() + { + var cut = RenderComponent(); + var initialNodes = cut.Nodes; + + cut.Find("button").Click(); + + cut.Instance.RenderCount.ShouldBe(2); + Assert.Same(initialNodes, cut.Nodes); + } + + } } diff --git a/tests/SampleComponents/RenderOnClick.razor b/tests/SampleComponents/RenderOnClick.razor new file mode 100644 index 000000000..d5ed71b16 --- /dev/null +++ b/tests/SampleComponents/RenderOnClick.razor @@ -0,0 +1,9 @@ + + +@code { + public int RenderCount { get; private set; } + + void IncreaseCount() { } + + protected override void OnAfterRender(bool firstRender) => RenderCount++; +} \ No newline at end of file From aed5ff30f68056b3fff8d56746c8b88efd6bf5ee Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Fri, 24 Jan 2020 09:05:19 +0000 Subject: [PATCH 30/30] Tweaks to tests --- .../BlazorElementReferencesIncludedInRenderedMarkup.razor | 1 - tests/_Imports.razor | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/Components/TestComponentBaseTest/BlazorElementReferencesIncludedInRenderedMarkup.razor b/tests/Components/TestComponentBaseTest/BlazorElementReferencesIncludedInRenderedMarkup.razor index 3133f8108..b74d0f841 100644 --- a/tests/Components/TestComponentBaseTest/BlazorElementReferencesIncludedInRenderedMarkup.razor +++ b/tests/Components/TestComponentBaseTest/BlazorElementReferencesIncludedInRenderedMarkup.razor @@ -1,6 +1,5 @@ @inherits TestComponentBase -
diff --git a/tests/_Imports.razor b/tests/_Imports.razor index 94baed4ea..7c7ad8e8b 100644 --- a/tests/_Imports.razor +++ b/tests/_Imports.razor @@ -1,5 +1,9 @@ @using Microsoft.AspNetCore.Components.Web @using Microsoft.Extensions.DependencyInjection +@using Egil.RazorComponents.Testing +@using Egil.RazorComponents.Testing.Asserting +@using Egil.RazorComponents.Testing.EventDispatchExtensions +@using Egil.RazorComponents.Testing.Mocking.JSInterop @using Egil.RazorComponents.Testing.SampleComponents @using Egil.RazorComponents.Testing.SampleComponents.Data @using Shouldly