diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6864fc111..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 @@ -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 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/ 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 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/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/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/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/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/RazorTestComponents/Components/AlertRazorTest.razor b/sample/tests/RazorTestComponents/Components/AlertRazorTest.razor index 2223bbdd6..9b83d74ff 100644 --- a/sample/tests/RazorTestComponents/Components/AlertRazorTest.razor +++ b/sample/tests/RazorTestComponents/Components/AlertRazorTest.razor @@ -1,7 +1,7 @@ -@inherits TestComponentBase +@inherits TestComponentBase @code { - MockJsRuntimeInvokeHandler MockJsRuntime { get; set; } + MockJsRuntimeInvokeHandler MockJsRuntime { get; set; } = default!; void Setup() { 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/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/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 9067e065b..66fcba79e 100644 --- a/sample/tests/Tests/Components/AlertTest.cs +++ b/sample/tests/Tests/Components/AlertTest.cs @@ -1,7 +1,8 @@ -using System; +using System; 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; @@ -208,7 +209,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 diff --git a/sample/tests/Tests/Components/FocussingInputTest.cs b/sample/tests/Tests/Components/FocussingInputTest.cs index 98e7df81e..bc84d316b 100644 --- a/sample/tests/Tests/Components/FocussingInputTest.cs +++ b/sample/tests/Tests/Components/FocussingInputTest.cs @@ -4,7 +4,9 @@ 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; namespace Egil.RazorComponents.Testing.SampleApp.CodeOnlyTests.Components @@ -24,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 e7478c734..a0946c64b 100644 --- a/sample/tests/Tests/Components/TodoListTest.cs +++ b/sample/tests/Tests/Components/TodoListTest.cs @@ -1,7 +1,9 @@ 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; using Egil.RazorComponents.Testing.SampleApp.Data; using Microsoft.AspNetCore.Components; @@ -119,8 +121,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/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/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 4b2a8d793..f80e30b9b 100644 --- a/sample/tests/_Imports.razor +++ b/sample/tests/_Imports.razor @@ -1,7 +1,9 @@ -@using Microsoft.AspNetCore.Components.Web +@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.SampleApp diff --git a/src/Assembly.cs b/src/Assembly.cs new file mode 100644 index 000000000..1dcfc7065 --- /dev/null +++ b/src/Assembly.cs @@ -0,0 +1 @@ +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Egil.RazorComponents.Testing.Library.Tests")] diff --git a/src/Asserting/GenericAssertExtensions.cs b/src/Asserting/CollectionAssertExtensions.cs similarity index 96% rename from src/Asserting/GenericAssertExtensions.cs rename to src/Asserting/CollectionAssertExtensions.cs index c6a0f007f..5d3609797 100644 --- a/src/Asserting/GenericAssertExtensions.cs +++ b/src/Asserting/CollectionAssertExtensions.cs @@ -8,9 +8,9 @@ namespace Egil.RazorComponents.Testing.Asserting { /// - /// Generic test assertions + /// Collection test assertions /// - public static class GenericAssertExtensions + public static class CollectionAssertExtensions { /// /// Verifies that a collection contains exactly a given number of elements, which diff --git a/src/Asserting/CompareToDiffingExtensions.cs b/src/Asserting/CompareToDiffingExtensions.cs index 0c6919d1f..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.HtmlParser.Parse(expected); + 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/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/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/src/Asserting/MarkupMatchesAssertExtensions.cs b/src/Asserting/MarkupMatchesAssertExtensions.cs index 87bf3a483..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.HtmlParser.Parse(expected); + 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 93c79711d..e91cb17ad 100644 --- a/src/Asserting/ShouldBeTextChangeAssertExtensions.cs +++ b/src/Asserting/ShouldBeTextChangeAssertExtensions.cs @@ -10,13 +10,28 @@ namespace Egil.RazorComponents.Testing.Asserting { + /// + /// Verification helpers for text + /// 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)); @@ -29,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); + ShouldBeTextChange(actualChange, expectedChange.Nodes, 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/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)); } diff --git a/src/Components/Fixture.cs b/src/Components/Fixture.cs index d196ec371..3a052007e 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 = NoopTestMethodAsync; private Action _test = NoopTestMethod; + private Func _testAsync = NoopTestMethodAsync; 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 ?? NoopTestMethodAsync; } + /// /// 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 ?? NoopTestMethodAsync; } + /// /// 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,14 @@ public class Fixture : FragmentBase /// [Parameter] public IReadOnlyCollection Tests { get => _tests; set => _tests = value ?? Array.Empty(); } - private static void NoopTestMethod() { } + /// + /// 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>(); } } } 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..a57b91c10 100644 --- a/src/Components/FragmentBase.cs +++ b/src/Components/FragmentBase.cs @@ -4,12 +4,23 @@ namespace Egil.RazorComponents.Testing { + /// + /// Represents a fragment that can be used in or . + /// public abstract class FragmentBase : IComponent { + internal static void NoopTestMethod() { } + internal static Task NoopTestMethodAsync() => Task.CompletedTask; + + /// + /// 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/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 2969e0031..66d57cbd7 100644 --- a/src/Components/TestComponentBase.cs +++ b/src/Components/TestComponentBase.cs @@ -2,6 +2,8 @@ using System.Linq; 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; @@ -32,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() { @@ -52,15 +50,15 @@ 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); - ExecuteSnapshotTests(container); + await ExecuteFixtureTests(container).ConfigureAwait(false); + await ExecuteSnapshotTests(container).ConfigureAwait(false); } - + /// public IRenderedFragment GetComponentUnderTest() => _testContextAdapter.GetComponentUnderTest(); @@ -77,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 @@ -92,7 +96,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 +106,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,7 +136,19 @@ private static void InvokeFixtureAction(Fixture fixture, Action action) } } - private void ExecuteSnapshotTests(ContainerComponent container) + 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 async Task ExecuteSnapshotTests(ContainerComponent container) { foreach (var (_, snapshot) in container.GetComponents()) { @@ -134,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/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/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); } } 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/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/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/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/Asserting/JsInvokeCountExpectedException.cs b/src/Mocking/JSInterop/JsInvokeCountExpectedException.cs similarity index 53% rename from src/Asserting/JsInvokeCountExpectedException.cs rename to src/Mocking/JSInterop/JsInvokeCountExpectedException.cs index 636bf70ad..058902d5a 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.Mocking.JSInterop; 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 53% rename from src/Asserting/JsRuntimeAssertExtensions.cs rename to src/Mocking/JSInterop/JsRuntimeAssertExtensions.cs index 4fffda259..e78882cfa 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.Mocking.JSInterop { /// /// 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,25 @@ 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. + /// 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) { if (handler is null) throw new ArgumentNullException(nameof(handler)); if (calledTimes < 1) @@ -40,17 +62,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/JSInterop/JsRuntimeInvocation.cs b/src/Mocking/JSInterop/JsRuntimeInvocation.cs index a12d0095a..117953aa1 100644 --- a/src/Mocking/JSInterop/JsRuntimeInvocation.cs +++ b/src/Mocking/JSInterop/JsRuntimeInvocation.cs @@ -1,12 +1,14 @@ using System.Collections.Generic; using System; +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 /// + [SuppressMessage("Design", "CA1068:CancellationToken parameters must come last", Justification = "")] public readonly struct JsRuntimeInvocation : IEquatable { /// @@ -24,29 +26,57 @@ namespace Egil.RazorComponents.Testing /// public IReadOnlyList Arguments { get; } + /// /// 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/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..4c3a796c1 100644 --- a/src/Mocking/JSInterop/JsRuntimePlannedInvocation.cs +++ b/src/Mocking/JSInterop/JsRuntimePlannedInvocation.cs @@ -3,18 +3,57 @@ using System; using System.Diagnostics.CodeAnalysis; -namespace Egil.RazorComponents.Testing +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/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/MockJsRuntimeInvokeHandler.cs b/src/Mocking/JSInterop/MockJsRuntimeInvokeHandler.cs index 996da6147..51f9efb0c 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 . @@ -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/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/Mocking/JSInterop/UnplannedJsInvocationException.cs b/src/Mocking/JSInterop/UnplannedJsInvocationException.cs index c4b3b3689..f1c30f3aa 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 @@ -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/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 1affd5f4e..153e5b9dd 100644 --- a/src/Mocking/MockJsRuntimeExtensions.cs +++ b/src/Mocking/MockJsRuntimeExtensions.cs @@ -1,16 +1,25 @@ using System; +using Egil.RazorComponents.Testing.Mocking.JSInterop; +using Microsoft.Extensions.DependencyInjection; 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)); var result = new MockJsRuntimeInvokeHandler(mode); - serviceProvider.AddService(result.ToJsRuntime()); + serviceProvider.AddSingleton(result.ToJsRuntime()); return result; } 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/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/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 7ffbbbb29..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) - _snapshotNodes = TestContext.HtmlParser.Parse(_snapshotMarkup); + if (_snapshotNodes is null) + _snapshotNodes = TestContext.CreateNodes(_snapshotMarkup); - return GetNodes().CompareTo(_snapshotNodes); + return Nodes.CompareTo(_snapshotNodes); } @@ -76,25 +98,8 @@ public IReadOnlyList GetChangesSinceSnapshot() public IReadOnlyList GetChangesSinceFirstRender() { if (_firstRenderNodes is null) - _firstRenderNodes = TestContext.HtmlParser.Parse(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.HtmlParser.Parse(GetMarkup()); - return _latestRenderNodes; + _firstRenderNodes = TestContext.CreateNodes(FirstRenderMarkup); + return Nodes.CompareTo(_firstRenderNodes); } private void ComponentMarkupChanged(in RenderBatch renderBatch) 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) { diff --git a/src/TestContext.cs b/src/TestContext.cs index 85561e2ce..72d8c62b2 100644 --- a/src/TestContext.cs +++ b/src/TestContext.cs @@ -1,9 +1,12 @@ -using Egil.RazorComponents.Testing.Diffing; +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; @@ -23,9 +26,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(); @@ -34,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(() => @@ -45,6 +47,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/src/TestServiceProvider.cs b/src/TestServiceProvider.cs index 79f15a98d..6f46f29c9 100644 --- a/src/TestServiceProvider.cs +++ b/src/TestServiceProvider.cs @@ -1,6 +1,8 @@ -using Microsoft.Extensions.DependencyInjection; -using System; +using System; +using System.Collections; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; namespace Egil.RazorComponents.Testing { @@ -8,175 +10,126 @@ 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; + /// + /// Gets a reusable default test service provider. + /// + public static readonly IServiceProvider Default = new TestServiceProvider(new ServiceCollection(), true); + /// /// Gets whether this has been initialized, and /// no longer will accept calls to the AddService's methods. /// 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 . + /// Creates an instance of the and sets its service collection to the + /// provided , if any. /// - /// 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/Razor.Components.Testing.Library.Template.csproj b/template/Egil.Razor.Components.Testing.Library.Template.csproj similarity index 91% rename from template/Razor.Components.Testing.Library.Template.csproj rename to template/Egil.Razor.Components.Testing.Library.Template.csproj index 4c6922876..64e582ac4 100644 --- a/template/Razor.Components.Testing.Library.Template.csproj +++ b/template/Egil.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/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..d248d537f 100644 --- a/template/template/_Imports.razor +++ b/template/template/_Imports.razor @@ -1,6 +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/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/Asserting/CompareToDiffingExtensionsTest.cs b/tests/Asserting/CompareToDiffingExtensionsTest.cs new file mode 100644 index 000000000..6f2c60448 --- /dev/null +++ b/tests/Asserting/CompareToDiffingExtensionsTest.cs @@ -0,0 +1,89 @@ +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.Markup).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.Nodes).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.Nodes.CompareTo(elm).Count.ShouldBe(1); + } + } +} 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/GenericCollectionAssertExtensionsTest.cs b/tests/Asserting/GenericCollectionAssertExtensionsTest.cs new file mode 100644 index 000000000..bdf6857bc --- /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.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.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.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.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); } + ); + } + + } +} diff --git a/tests/AllTypesOfParamsTest.cs b/tests/ComponentTestFixtureTest.cs similarity index 90% rename from tests/AllTypesOfParamsTest.cs rename to tests/ComponentTestFixtureTest.cs index 6be21847d..fd5ec5e35 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() @@ -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/BlazorElementReferencesIncludedInRenderedMarkup.razor b/tests/Components/TestComponentBaseTest/BlazorElementReferencesIncludedInRenderedMarkup.razor similarity index 54% rename from tests/BlazorElementReferencesIncludedInRenderedMarkup.razor rename to tests/Components/TestComponentBaseTest/BlazorElementReferencesIncludedInRenderedMarkup.razor index c3fd541d9..3133f8108 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 html = cut.GetMarkup(); + var cut = GetFragment(); + + var html = cut.Markup; 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 86% rename from tests/CorrectImplicitRazorTestContextAvailable.razor rename to tests/Components/TestComponentBaseTest/CorrectImplicitRazorTestContextAvailable.razor index 03d04b0e4..f5fdad84c 100644 --- a/tests/CorrectImplicitRazorTestContextAvailable.razor +++ b/tests/Components/TestComponentBaseTest/CorrectImplicitRazorTestContextAvailable.razor @@ -14,11 +14,12 @@ void Setup1() { - Services.AddService(dep1Expected); + Services.AddSingleton(dep1Expected); } 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; + } } @@ -44,7 +52,7 @@ void Setup2() { - Services.AddService(dep2Expected); + Services.AddSingleton(dep2Expected); } void Test2() @@ -78,7 +86,7 @@ } } - +

@nameof(Dep3)

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 95% rename from tests/GettingCutAndFragmentFromRazorTestContextTest.razor rename to tests/Components/TestComponentBaseTest/GettingCutAndFragmentFromRazorTestContextTest.razor index 5af2ceb24..fc27ce7f0 100644 --- a/tests/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/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/Components/TestComponentBaseTest/SnapshotTestTest.razor b/tests/Components/TestComponentBaseTest/SnapshotTestTest.razor new file mode 100644 index 000000000..46049852e --- /dev/null +++ b/tests/Components/TestComponentBaseTest/SnapshotTestTest.razor @@ -0,0 +1,19 @@ +@inherits TestComponentBase +@using Shouldly + +@code { + class Dep1 : ITestDep { public string Name { get; } = "FOO"; } + class Dep2 : IAsyncTestDep { public Task GetData() => Task.FromResult("BAR"); } +} + + + + + + + +

FOO

+

BAR

+
+
\ 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/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)); + } + } } 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 241c968a9..8a82ddb38 100644 --- a/tests/GlobalSuppressions.cs +++ b/tests/GlobalSuppressions.cs @@ -3,3 +3,5 @@ [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")] +[assembly: SuppressMessage("Usage", "CA2234:Pass system uri objects instead of strings", Justification = "")] diff --git a/tests/JSInterop/MockJsRuntimeInvokeHandlerTest.cs b/tests/JSInterop/MockJsRuntimeInvokeHandlerTest.cs deleted file mode 100644 index 3c43800c9..000000000 --- a/tests/JSInterop/MockJsRuntimeInvokeHandlerTest.cs +++ /dev/null @@ -1,61 +0,0 @@ -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.JSInterop -{ - public class MockJsRuntimeInvokeHandlerTest - { - [Fact(DisplayName = "Mock returns default value in loose mode without invocation setup")] - public async Task Test001() - { - var sut = new MockJsRuntimeInvokeHandler(JsRuntimeMockMode.Loose); - - var result = await sut.ToJsRuntime().InvokeAsync("ident", Array.Empty()); - - result.ShouldBe(default); - } - - [Fact(DisplayName = "After invocation a invocation should be visible from the Invocations list")] - public void Test002() - { - var identifier = "fooFunc"; - var args = new[] { "bar", "baz" }; - using var cts = new CancellationTokenSource(); - var sut = new MockJsRuntimeInvokeHandler(JsRuntimeMockMode.Loose); - - sut.ToJsRuntime().InvokeAsync(identifier, cts.Token, args); - - var invocation = sut.Invocations[identifier].Single(); - invocation.Identifier.ShouldBe(identifier); - invocation.Arguments.ShouldBe(args); - invocation.CancellationToken.ShouldBe(cts.Token); - } - - [Fact(DisplayName = "Mock throws exception when in strict mode and invocation has not been setup")] - public void Test003() - { - var sut = new MockJsRuntimeInvokeHandler(JsRuntimeMockMode.Strict); - - Should.Throw(() => sut.ToJsRuntime().InvokeAsync("ident", new[] { "bar", "baz" })); - } - - [Fact(DisplayName = "Mock returns task from planned invocation when one is present")] - public async Task Test004() - { - var expectedResult = "HELLO WORLD"; - var ident = "fooFunc"; - var sut = new MockJsRuntimeInvokeHandler(JsRuntimeMockMode.Strict); - sut.Setup(ident).SetResult(expectedResult); - - var result = await sut.ToJsRuntime().InvokeAsync(ident, Array.Empty()); - - result.ShouldBe(expectedResult); - } - } -} diff --git a/tests/Mocking/JSInterop/JsRuntimeAssertExtensionsTest.cs b/tests/Mocking/JSInterop/JsRuntimeAssertExtensionsTest.cs new file mode 100644 index 000000000..e0b0d71ee --- /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 = (IElement)htmlParser.Parse($"

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

").First(); + + Should.Throw(() => elmRef.ShouldBeElementReferenceTo(elmWithoutRefAttr)); + } + } +} 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/Mocking/JSInterop/MockJsRuntimeInvokeHandlerTest.cs b/tests/Mocking/JSInterop/MockJsRuntimeInvokeHandlerTest.cs new file mode 100644 index 000000000..9325051bc --- /dev/null +++ b/tests/Mocking/JSInterop/MockJsRuntimeInvokeHandlerTest.cs @@ -0,0 +1,239 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.JSInterop; +using Shouldly; +using Xunit; + +namespace Egil.RazorComponents.Testing.Mocking.JSInterop +{ + public class MockJsRuntimeInvokeHandlerTest + { + [Fact(DisplayName = "Mock returns default value in loose mode without invocation setup")] + public async Task Test001() + { + var sut = new MockJsRuntimeInvokeHandler(JsRuntimeMockMode.Loose); + + var result = await sut.ToJsRuntime().InvokeAsync("ident", Array.Empty()); + + result.ShouldBe(default); + } + + [Fact(DisplayName = "After invocation a invocation should be visible from the Invocations list")] + public void Test002() + { + var identifier = "fooFunc"; + var args = new[] { "bar", "baz" }; + using var cts = new CancellationTokenSource(); + var sut = new MockJsRuntimeInvokeHandler(JsRuntimeMockMode.Loose); + + sut.ToJsRuntime().InvokeAsync(identifier, cts.Token, args); + + var invocation = sut.Invocations[identifier].Single(); + invocation.Identifier.ShouldBe(identifier); + invocation.Arguments.ShouldBe(args); + invocation.CancellationToken.ShouldBe(cts.Token); + } + + [Fact(DisplayName = "Mock throws exception when in strict mode and invocation has not been setup")] + 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) + ); + + 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 = "SetupVoid returns a planned invocation that does not take a result object")] + public async Task Test012() + { + var identifier = "func"; + var sut = new MockJsRuntimeInvokeHandler(JsRuntimeMockMode.Strict); + 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() + ); + + await Should.ThrowAsync( + sut.ToJsRuntime().InvokeVoidAsync("foo").AsTask() + ); + + 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); + } + } +} 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/RenderComponentTest.cs b/tests/RenderComponentTest.cs new file mode 100644 index 000000000..33379530d --- /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 = "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.Nodes; + + cut.Render(); + cut.SetParametersAndRender(ChildContent("
")); + + Assert.Same(initialNodes, cut.Nodes); + } + + [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.Nodes; + + cut.SetParametersAndRender(ChildContent("

")); + + Assert.NotSame(initialNodes, cut.Nodes); + cut.Find("p").ShouldNotBeNull(); + } + + [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.Nodes; + + cut.Render(); + + 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 2ddaf7cc6..ac8075e5e 100644 --- a/tests/RenderedFragmentTest.cs +++ b/tests/RenderedFragmentTest.cs @@ -1,7 +1,7 @@ -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 Microsoft.Extensions.DependencyInjection; using Shouldly; using System; using System.Collections.Generic; @@ -28,104 +28,48 @@ 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); + Services.AddSingleton(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); } - - } - - 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/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/TestServiceProviderTest.cs b/tests/TestServiceProviderTest.cs new file mode 100644 index 000000000..f16208f9d --- /dev/null +++ b/tests/TestServiceProviderTest.cs @@ -0,0 +1,148 @@ +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; +using Xunit; + +namespace Egil.RazorComponents.Testing +{ + public class TestServiceProviderTest : ComponentTestFixture + { + 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()); + ex.InnerException.ShouldBeOfType(); + } + + [Fact(DisplayName = "The placeholder IJSRuntime is overriden by a supplied mock and does not throw")] + public void Test022() + { + Services.AddMockJsRuntime(); + + RenderComponent(); + } + } +} diff --git a/tests/TestUtililities/MockingHelpers.cs b/tests/TestUtililities/MockingHelpers.cs new file mode 100644 index 000000000..8ab6fd030 --- /dev/null +++ b/tests/TestUtililities/MockingHelpers.cs @@ -0,0 +1,69 @@ +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) + { + if (type is null) throw new ArgumentNullException(nameof(type)); + + 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 is null) throw new ArgumentNullException(nameof(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); + } + } +} 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