diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 8d34bc739..7be495583 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -32,22 +32,10 @@ jobs: - uses: actions/setup-dotnet@v1 with: - dotnet-version: '3.1.x' + dotnet-version: '3.1.x' - uses: actions/setup-dotnet@v1 with: dotnet-version: '5.0.100-rc.1.20452.10' - - name: DOTNET HACK - shell: pwsh - run: | - $version = Split-Path (Split-Path $ENV:DOTNET_ROOT -Parent) -Leaf; - $root = Split-Path (Split-Path $ENV:DOTNET_ROOT -Parent) -Parent; - $directories = Get-ChildItem $root | Where-Object { $_.Name -ne $version }; - foreach ($dir in $directories) { - $from = $dir.FullName; - $to = "$root/$version"; - Write-Host Copying from $from to $to; - Copy-Item "$from\*" $to -Recurse -Force; - } - name: Building library run: dotnet build /p:PublicRelease=true diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 833646776..5a36191f4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -40,18 +40,6 @@ jobs: - uses: actions/setup-dotnet@v1 with: dotnet-version: '5.0.100-rc.1.20452.10' - - name: Move .net SDK's to shared folder (hack) - shell: pwsh - run: | - $version = Split-Path (Split-Path $ENV:DOTNET_ROOT -Parent) -Leaf; - $root = Split-Path (Split-Path $ENV:DOTNET_ROOT -Parent) -Parent; - $directories = Get-ChildItem $root | Where-Object { $_.Name -ne $version }; - foreach ($dir in $directories) { - $from = $dir.FullName; - $to = "$root/$version"; - Write-Host Copying from $from to $to; - Copy-Item "$from\*" $to -Recurse -Force; - } - name: Building library in release mode run: dotnet build -c Release -p:ContinuousIntegrationBuild=true @@ -71,18 +59,6 @@ jobs: - uses: actions/setup-dotnet@v1 with: dotnet-version: '5.0.100-rc.1.20452.10' - - name: Move .net SDK's to shared folder (hack) - shell: pwsh - run: | - $version = Split-Path (Split-Path $ENV:DOTNET_ROOT -Parent) -Leaf; - $root = Split-Path (Split-Path $ENV:DOTNET_ROOT -Parent) -Parent; - $directories = Get-ChildItem $root | Where-Object { $_.Name -ne $version }; - foreach ($dir in $directories) { - $from = $dir.FullName; - $to = "$root/$version"; - Write-Host Copying from $from to $to; - Copy-Item "$from\*" $to -Recurse -Force; - } - name: Running unit tests run: | @@ -124,18 +100,6 @@ jobs: - uses: actions/setup-dotnet@v1 with: dotnet-version: '5.0.100-rc.1.20452.10' - - name: Move .net SDK's to shared folder (hack) - shell: pwsh - run: | - $version = Split-Path (Split-Path $ENV:DOTNET_ROOT -Parent) -Leaf; - $root = Split-Path (Split-Path $ENV:DOTNET_ROOT -Parent) -Parent; - $directories = Get-ChildItem $root | Where-Object { $_.Name -ne $version }; - foreach ($dir in $directories) { - $from = $dir.FullName; - $to = "$root/$version"; - Write-Host Copying from $from to $to; - Copy-Item "$from\*" $to -Recurse -Force; - } - name: Creating library package run: | @@ -169,18 +133,6 @@ jobs: - uses: actions/setup-dotnet@v1 with: dotnet-version: '5.0.100-rc.1.20452.10' - - name: Move .net SDK's to shared folder (hack) - shell: pwsh - run: | - $version = Split-Path (Split-Path $ENV:DOTNET_ROOT -Parent) -Leaf; - $root = Split-Path (Split-Path $ENV:DOTNET_ROOT -Parent) -Parent; - $directories = Get-ChildItem $root | Where-Object { $_.Name -ne $version }; - foreach ($dir in $directories) { - $from = $dir.FullName; - $to = "$root/$version"; - Write-Host Copying from $from to $to; - Copy-Item "$from\*" $to -Recurse -Force; - } - name: Install dotnet-format run: dotnet tool install -g dotnet-format @@ -238,18 +190,6 @@ jobs: - uses: actions/setup-dotnet@v1 with: dotnet-version: '5.0.100-rc.1.20452.10' - - name: Move .net SDK's to shared folder (hack) - shell: pwsh - run: | - $version = Split-Path (Split-Path $ENV:DOTNET_ROOT -Parent) -Leaf; - $root = Split-Path (Split-Path $ENV:DOTNET_ROOT -Parent) -Parent; - $directories = Get-ChildItem $root | Where-Object { $_.Name -ne $version }; - foreach ($dir in $directories) { - $from = $dir.FullName; - $to = "$root/$version"; - Write-Host Copying from $from to $to; - Copy-Item "$from\*" $to -Recurse -Force; - } - name: Initialize CodeQL uses: github/codeql-action/init@v1 @@ -296,18 +236,6 @@ jobs: - uses: actions/setup-dotnet@v1 with: dotnet-version: '5.0.100-rc.1.20452.10' - - name: DOTNET HACK - shell: pwsh - run: | - $version = Split-Path (Split-Path $ENV:DOTNET_ROOT -Parent) -Leaf; - $root = Split-Path (Split-Path $ENV:DOTNET_ROOT -Parent) -Parent; - $directories = Get-ChildItem $root | Where-Object { $_.Name -ne $version }; - foreach ($dir in $directories) { - $from = $dir.FullName; - $to = "$root/$version"; - Write-Host Copying from $from to $to; - Copy-Item "$from\*" $to -Recurse -Force; - } - name: Creating library package for pre-release if: github.event_name != 'release' diff --git a/CHANGELOG.md b/CHANGELOG.md index 0495c517e..9fcd216c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,31 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### Added List of new features. +- Two new overloads to the `RenderFragment()` and `ChildContent()` component parameter factory methods have been added that takes a `RenderFragment` as input. By [@egil](https://github.com/egil) in [#203](https://github.com/egil/bUnit/pull/203). +- Added a `ComponentParameterCollection` type. The `ComponentParameterCollection` is a collection of component parameters, that knows how to turn those components parameters into a `RenderFragment`, which will render a component and pass any parameters inside the collection to that component. That logic was spread out over multiple places in bUnit, and is now owned by the `ComponentParameterCollection` type. By [@egil](https://github.com/egil) in [#203](https://github.com/egil/bUnit/pull/203). + ### Changed List of changes in existing functionality. +- The `ComponentParameterBuilder` has been renamed to `ComponentParameterCollectionBuilder`, since it now builds the `ComponentParameterCollection` type, introduced in this release of bUnit. By [@egil](https://github.com/egil) in [#203](https://github.com/egil/bUnit/pull/203). +- `ComponentParameterCollectionBuilder` now allows adding cascading values that is not directly used by the component type it targets. This makes it possible to add cascading values to children of the target component. By [@egil](https://github.com/egil) in [#203](https://github.com/egil/bUnit/pull/203). +- The `Add(object)` has been replaced by `AddCascadingValue(object)` in `ComponentParameterCollectionBuilder`, to make it more clear that an unnnamed cascading value is being passed to the target component or one of its child components. It it is also possible to pass unnamed cascading values using the `Add(parameterSelector, value)` method, which now correctly detect if the selected cascading value parameter is named or unnamed. By [@egil](https://github.com/egil) in [#203](https://github.com/egil/bUnit/pull/203). +- It is now possible to call the `Add()`, `AddChildContent()` methods on `ComponentParameterCollectionBuilder`, and the factory methods `RenderFragment()`, `ChildContent()`, and `Template()`, _**multiple times**_ for the same parameter, if it is of type `RenderFragment` or `RenderFragment`. Doing so previously would either result in an exception or just the last passed `RenderFragment` to be used. Now all the provided `RenderFragment` or `RenderFragment` will be combined at runtime into a single `RenderFragment` or `RenderFragment`. + + For example, this makes it easier to pass e.g. both a markup string and a component to a `ChildContent` parameter: + + ```csharp + var cut = ctx.RenderComponent(parameters => parameters + .AddChildContent("

Below you will find a most interesting alert!

") + .AddChildContent(childParams => childParams + .Add(p => p.Heading, "Alert heading") + .Add(p => p.Type, AlertType.Warning) + .AddChildContent("

Hello World

") + ) + ); + ``` + By [@egil](https://github.com/egil) in [#203](https://github.com/egil/bUnit/pull/203). + ### Deprecated List of soon-to-be removed features. @@ -21,6 +43,8 @@ List of now removed features. ### Fixed List of any bug fixes. +- Using the ComponentParameterCollectionBuilder's `Add(p => p.Param, value)` method to add a unnamed cascading value didn't create an unnnamed cascading value parameter. By [@egil](https://github.com/egil) in [#203](https://github.com/egil/bUnit/pull/203). Credits to [Ben Sampica (@benjaminsampica)](https://github.com/benjaminsampica) for reporting and helping investigate this issue. + ### Security List of fixed security vulnerabilities. diff --git a/docs/samples/tests/Directory.Build.props b/docs/samples/tests/Directory.Build.props index c02db0d20..09eee9266 100644 --- a/docs/samples/tests/Directory.Build.props +++ b/docs/samples/tests/Directory.Build.props @@ -2,7 +2,7 @@ netcoreapp3.1;net5.0 false - 8.0 + 9.0 3.0 diff --git a/docs/samples/tests/mstest/BunitTestContext.cs b/docs/samples/tests/mstest/BunitTestContext.cs index e384d067b..19bf0a1cd 100644 --- a/docs/samples/tests/mstest/BunitTestContext.cs +++ b/docs/samples/tests/mstest/BunitTestContext.cs @@ -29,7 +29,7 @@ public void Dispose() public IRenderedComponent RenderComponent(params ComponentParameter[] parameters) where TComponent : IComponent => _context?.RenderComponent(parameters) ?? throw new InvalidOperationException("MSTest has not started executing tests yet"); - public IRenderedComponent RenderComponent(Action> parameterBuilder) where TComponent : IComponent + public IRenderedComponent RenderComponent(Action> parameterBuilder) where TComponent : IComponent => _context?.RenderComponent(parameterBuilder) ?? throw new InvalidOperationException("MSTest has not started executing tests yet"); } } \ No newline at end of file diff --git a/docs/samples/tests/mstest/bunit.docs.mstest.samples.csproj b/docs/samples/tests/mstest/bunit.docs.mstest.samples.csproj index 1c6f7e41c..bbccf8563 100644 --- a/docs/samples/tests/mstest/bunit.docs.mstest.samples.csproj +++ b/docs/samples/tests/mstest/bunit.docs.mstest.samples.csproj @@ -12,7 +12,6 @@ - diff --git a/docs/samples/tests/nunit/BunitTestContext.cs b/docs/samples/tests/nunit/BunitTestContext.cs index 178496aac..15afa10bb 100644 --- a/docs/samples/tests/nunit/BunitTestContext.cs +++ b/docs/samples/tests/nunit/BunitTestContext.cs @@ -29,7 +29,7 @@ public void Dispose() public IRenderedComponent RenderComponent(params ComponentParameter[] parameters) where TComponent : IComponent => _context?.RenderComponent(parameters) ?? throw new InvalidOperationException("NUnit has not started executing tests yet"); - public IRenderedComponent RenderComponent(Action> parameterBuilder) where TComponent : IComponent + public IRenderedComponent RenderComponent(Action> parameterBuilder) where TComponent : IComponent => _context?.RenderComponent(parameterBuilder) ?? throw new InvalidOperationException("NUnit has not started executing tests yet"); } } \ No newline at end of file diff --git a/docs/samples/tests/nunit/bunit.docs.nunit.samples.csproj b/docs/samples/tests/nunit/bunit.docs.nunit.samples.csproj index 91202ed62..5c5ca23c6 100644 --- a/docs/samples/tests/nunit/bunit.docs.nunit.samples.csproj +++ b/docs/samples/tests/nunit/bunit.docs.nunit.samples.csproj @@ -11,7 +11,6 @@ - diff --git a/docs/samples/tests/razor/AllKindsOfParamsTest.razor b/docs/samples/tests/razor/AllKindsOfParamsTest.razor index 5236e108a..6ec6f33db 100644 --- a/docs/samples/tests/razor/AllKindsOfParamsTest.razor +++ b/docs/samples/tests/razor/AllKindsOfParamsTest.razor @@ -109,14 +109,14 @@ + + diff --git a/docs/samples/tests/razor/bunit.docs.razor.samples.csproj b/docs/samples/tests/razor/bunit.docs.razor.samples.csproj index 6d6427d75..9ac6b5d77 100644 --- a/docs/samples/tests/razor/bunit.docs.razor.samples.csproj +++ b/docs/samples/tests/razor/bunit.docs.razor.samples.csproj @@ -6,9 +6,6 @@ - - - @@ -18,6 +15,8 @@ + + diff --git a/docs/samples/tests/xunit/AllKindsOfParamsTest.cs b/docs/samples/tests/xunit/AllKindsOfParamsTest.cs index 756a00596..23b1fb4bf 100644 --- a/docs/samples/tests/xunit/AllKindsOfParamsTest.cs +++ b/docs/samples/tests/xunit/AllKindsOfParamsTest.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; +using Bunit.Rendering; using static Bunit.ComponentParameterFactory; @@ -125,17 +126,24 @@ public void ComponentAndMarkupAsChildContent() { using var ctx = new TestContext(); + // Using factory method + var cut1 = ctx.RenderComponent( + ChildContent("

Below you will find a most interesting alert!

"), + ChildContent( + ("Heading", "Alert heading"), + ("Type", AlertType.Warning), + ChildContent("

Hello World

") + ) + ); + // Using parameter builder - var cut = ctx.RenderComponent(parameters => parameters - .Add(p => p.ChildContent, (RenderFragment)(builder => - { - builder.AddMarkupContent(1, "

Below you will find a most interesting alert!

"); - builder.OpenComponent(2); - builder.AddAttribute(3, "Heading", "Alert heading"); - builder.AddAttribute(4, "Type", AlertType.Warning); - builder.AddAttribute(5, "ChildContent", (RenderFragment)(alertBuilder => alertBuilder.AddMarkupContent(1, "

Hello World

"))); - builder.CloseComponent(); - })) + var cut2 = ctx.RenderComponent(parameters => parameters + .AddChildContent("

Below you will find a most interesting alert!

") + .AddChildContent(childParams => childParams + .Add(p => p.Heading, "Alert heading") + .Add(p => p.Type, AlertType.Warning) + .AddChildContent("

Hello World

") + ) ); } @@ -200,17 +208,24 @@ public void ComponentAndMarkupAsRenderFragment() { using var ctx = new TestContext(); + // Using factory method + var cut1 = ctx.RenderComponent( + RenderFragment("Content", "

Below you will find a most interesting alert!

"), + RenderFragment("Content", + ("Heading", "Alert heading"), + ("Type", AlertType.Warning), + ChildContent("

Hello World

") + ) + ); + // Using parameter builder - var cut = ctx.RenderComponent(parameters => parameters - .Add(p => p.Content, (RenderFragment)(builder => - { - builder.AddMarkupContent(1, "

Below you will find a most interesting alert!

"); - builder.OpenComponent(2); - builder.AddAttribute(3, "Heading", "Alert heading"); - builder.AddAttribute(4, "Type", AlertType.Warning); - builder.AddAttribute(5, "ChildContent", (RenderFragment)(alertBuilder => alertBuilder.AddMarkupContent(1, "

Hello World

"))); - builder.CloseComponent(); - })) + var cut2 = ctx.RenderComponent(parameters => parameters + .Add(p => p.Content, "

Below you will find a most interesting alert!

") + .Add(p => p.Content, childParams => childParams + .Add(p => p.Heading, "Alert heading") + .Add(p => p.Type, AlertType.Warning) + .AddChildContent("

Hello World

") + ) ); } @@ -240,29 +255,17 @@ public void HtmlAndComponentTemplateParams() // Using factory method var cut1 = ctx.RenderComponent>( ("Items", new string[] { "Foo", "Bar", "Baz" }), - Template("Template", item => builder => - { - builder.OpenElement(1, "div"); - builder.AddAttribute(2, "class", "item"); - builder.OpenComponent(3); - builder.AddAttribute(4, "Value", item); - builder.CloseComponent(); - builder.CloseElement(); + Template("Template", value => new ComponentParameter[] { + ("Value", value) }) ); // Using parameter builder var cut2 = ctx.RenderComponent>(parameters => parameters .Add(p => p.Items, new[] { "Foo", "Bar", "Baz" }) - .Add(p => p.Template, item => builder => - { - builder.OpenElement(1, "div"); - builder.AddAttribute(2, "class", "item"); - builder.OpenComponent(3); - builder.AddAttribute(4, "Value", item); - builder.CloseComponent(); - builder.CloseElement(); - }) + .Add(p => p.Template, value => itemParams => itemParams + .Add(p => p.Value, value) + ) ); } @@ -295,7 +298,7 @@ public void UnnamedCascadingParamsTest() // Using parameter builder var cut2 = ctx.RenderComponent(parameters => parameters - .Add(isDarkTheme) + .AddCascadingValue(isDarkTheme) ); // Using parameter builder and selecting unnamed cascading parameter @@ -335,7 +338,7 @@ public void UnnamedAndNamedCascadingParamsTest() // Using parameter builder var cut2 = ctx.RenderComponent(parameters => parameters - .Add(isDarkTheme) + .AddCascadingValue(isDarkTheme) .Add(p => p.UserName, "Egil Hansen") .Add(p => p.Email, "egil@example.com") ); diff --git a/docs/samples/tests/xunit/bunit.docs.xunit.samples.csproj b/docs/samples/tests/xunit/bunit.docs.xunit.samples.csproj index 08c5a00b8..f11ec52ef 100644 --- a/docs/samples/tests/xunit/bunit.docs.xunit.samples.csproj +++ b/docs/samples/tests/xunit/bunit.docs.xunit.samples.csproj @@ -14,7 +14,6 @@ - diff --git a/docs/site/api/index.md b/docs/site/api/index.md deleted file mode 100644 index dd89abda5..000000000 --- a/docs/site/api/index.md +++ /dev/null @@ -1,3 +0,0 @@ -# bUnit References - -TODO: write intro \ No newline at end of file diff --git a/docs/site/docfx.json b/docs/site/docfx.json index d53f017d5..0e9df01c7 100644 --- a/docs/site/docfx.json +++ b/docs/site/docfx.json @@ -13,6 +13,9 @@ } ], "dest": "api", + "properties": { + "TargetFramework": "netstandard2.1" + }, "disableGitFeatures": false, "disableDefaultFilter": false } @@ -78,7 +81,7 @@ "markdownEngineName": "markdig", "noLangKeyword": false, "keepFileLink": false, - "cleanupCacheHistory": false, + "cleanupCacheHistory": true, "disableGitFeatures": false } } diff --git a/docs/site/docs.csproj b/docs/site/docs.csproj index 3fa8242f2..7764c8dc6 100644 --- a/docs/site/docs.csproj +++ b/docs/site/docs.csproj @@ -1,34 +1,20 @@ - + netcoreapp3.1 - 3.0 - - - - - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive + all + runtime; build; native; contentfiles; analyzers - + diff --git a/docs/site/docs/interaction/awaiting-async-state.md b/docs/site/docs/interaction/awaiting-async-state.md index 76e3b606e..6b5e6ebb5 100644 --- a/docs/site/docs/interaction/awaiting-async-state.md +++ b/docs/site/docs/interaction/awaiting-async-state.md @@ -9,11 +9,11 @@ A test can fail if a component performs asynchronous renders, e.g. because it wa This happens because tests execute in the test framework's synchronization context and the test renderer executes renders in its own synchronization context. -bUnit comes with two methods that helps deal with this issue, the [`WaitForState(Func, TimeSpan?)`](xref:Bunit.RenderedFragmentWaitForHelperExtensions.WaitForState(Bunit.IRenderedFragmentBase,System.Func{System.Boolean},System.Nullable{System.TimeSpan})) method covered on this page, and the [`WaitForAssertion(Action, TimeSpan?)`](xref:Bunit.RenderedFragmentWaitForHelperExtensions.WaitForAssertion(Bunit.IRenderedFragmentBase,System.Action,System.Nullable{System.TimeSpan})) method covered on the page. +bUnit comes with two methods that helps deal with this issue, the `WaitForState(Func, TimeSpan?)` method covered on this page, and the `WaitForAssertion(Action, TimeSpan?)` method covered on the page. ## Waiting for State Using `WaitForState` -The [`WaitForState(Func, TimeSpan?)`](xref:Bunit.RenderedFragmentWaitForHelperExtensions.WaitForState(Bunit.IRenderedFragmentBase,System.Func{System.Boolean},System.Nullable{System.TimeSpan})) method can be used to block and wait in a test method, until the provided predicate returns true, or the timeout is reached (the default timeout is one second). +The `WaitForState(Func, TimeSpan?)` method can be used to block and wait in a test method, until the provided predicate returns true, or the timeout is reached (the default timeout is one second). > [!NOTE] > The `WaitForState()` method will try the predicate pass to it when the `WaitForState()` method is called, and every time the component under test renders. @@ -34,7 +34,7 @@ This is what happens in the test: 4. Finally, the tests assertion step can execute, knowing that the desired state has been reached. > [!WARNING] -> The wait predicate and an assertion should not verify the same thing. Instead, use the [`WaitForAssertion(...)`](xref:Bunit.RenderedFragmentWaitForHelperExtensions.WaitForAssertion(Bunit.IRenderedFragmentBase,System.Action,System.Nullable{System.TimeSpan})) method covered on the page instead. +> The wait predicate and an assertion should not verify the same thing. Instead, use the `WaitForAssertion(...)` method covered on the page instead. ### Controlling Wait Timeout diff --git a/docs/site/docs/interaction/trigger-renders.md b/docs/site/docs/interaction/trigger-renders.md index f2b2a9d11..a40f93e17 100644 --- a/docs/site/docs/interaction/trigger-renders.md +++ b/docs/site/docs/interaction/trigger-renders.md @@ -5,7 +5,7 @@ title: Triggering a Render Life Cycle on a Component # Triggering a Render Life Cycle on a Component -When a component under test is rendered, an instance of the type is returned. Through that, it is possible to cause the component under test to render again directly through the [`Render()`](xref:Bunit.RenderedComponentRenderExtensions.Render``1(Bunit.IRenderedComponentBase{``0})) method or one of the [`SetParametersAndRender(...)`](xref:Bunit.RenderedComponentRenderExtensions.SetParametersAndRender``1(Bunit.IRenderedComponentBase{``0},System.Action{Bunit.ComponentParameterBuilder{``0}})) methods or indirectly through the [`InvokeAsync(...)`](xref:Bunit.RenderedComponentInvokeAsyncExtensions.InvokeAsync``1(Bunit.IRenderedComponentBase{``0},System.Action)) method. +When a component under test is rendered, an instance of the type is returned. Through that, it is possible to cause the component under test to render again directly through the [`Render()`](xref:Bunit.RenderedComponentRenderExtensions.Render``1(Bunit.IRenderedComponentBase{``0})) method or one of the [`SetParametersAndRender(...)`](xref:Bunit.RenderedComponentRenderExtensions.SetParametersAndRender``1(Bunit.IRenderedComponentBase{``0},System.Action{Bunit.ComponentParameterCollectionBuilder{``0}})) methods or indirectly through the [`InvokeAsync(...)`](xref:Bunit.RenderedComponentInvokeAsyncExtensions.InvokeAsync``1(Bunit.IRenderedComponentBase{``0},System.Action)) method. > [!WARNING] > The `Render()` and `SetParametersAndRender()` methods are not available in the type that is returned when calling the _non_-generic version of `GetComponentUnderTest()` in ``-based Razor tests. Call the generic version of `GetComponentUnderTest()` to get a . @@ -28,14 +28,14 @@ The highlighted line shows the call to [`Render()`](xref:Bunit.RenderedComponent ## SetParametersAndRender -The [`SetParametersAndRender(...)`](xref:Bunit.RenderedComponentRenderExtensions.SetParametersAndRender``1(Bunit.IRenderedComponentBase{``0},System.Action{Bunit.ComponentParameterBuilder{``0}})) methods tells the renderer to re-render the component with new parameters, i.e. go through its life-cycle methods (except for `OnInitialized()` and `OnInitializedAsync()` methods), passing the new parameters to the `SetParametersAsync()` method, _but only the new parameters_. To use it, do the following: +The [`SetParametersAndRender(...)`](xref:Bunit.RenderedComponentRenderExtensions.SetParametersAndRender``1(Bunit.IRenderedComponentBase{``0},System.Action{Bunit.ComponentParameterCollectionBuilder{``0}})) methods tells the renderer to re-render the component with new parameters, i.e. go through its life-cycle methods (except for `OnInitialized()` and `OnInitializedAsync()` methods), passing the new parameters to the `SetParametersAsync()` method, _but only the new parameters_. To use it, do the following: [!code-csharp[](../../../samples/tests/xunit/ReRenderTest.cs?start=31&end=42&highlight=8-10)] -The highlighted line shows the call to [`SetParametersAndRender(parameter builder)`](xref:Bunit.RenderedComponentRenderExtensions.SetParametersAndRender``1(Bunit.IRenderedComponentBase{``0},System.Action{Bunit.ComponentParameterBuilder{``0}})), which is also available as a version that takes the zero or more component parameters, e.g. created through the component parameter factory helper methods, if you prefer that method of passing parameters. +The highlighted line shows the call to [`SetParametersAndRender(parameter builder)`](xref:Bunit.RenderedComponentRenderExtensions.SetParametersAndRender``1(Bunit.IRenderedComponentBase{``0},System.Action{Bunit.ComponentParameterCollectionBuilder{``0}})), which is also available as a version that takes the zero or more component parameters, e.g. created through the component parameter factory helper methods, if you prefer that method of passing parameters. > [!NOTE] -> Passing parameters to components through the [`SetParametersAndRender(...)`](xref:Bunit.RenderedComponentRenderExtensions.SetParametersAndRender``1(Bunit.IRenderedComponentBase{``0},System.Action{Bunit.ComponentParameterBuilder{``0}})) methods is identical to doing it with the `RenderComponent(...)` methods, described in detail on the page. +> Passing parameters to components through the [`SetParametersAndRender(...)`](xref:Bunit.RenderedComponentRenderExtensions.SetParametersAndRender``1(Bunit.IRenderedComponentBase{``0},System.Action{Bunit.ComponentParameterCollectionBuilder{``0}})) methods is identical to doing it with the `RenderComponent(...)` methods, described in detail on the page. ## InvokeAsync diff --git a/docs/site/docs/providing-input/passing-parameters-to-components.md b/docs/site/docs/providing-input/passing-parameters-to-components.md index 1816c6d30..8e9309741 100644 --- a/docs/site/docs/providing-input/passing-parameters-to-components.md +++ b/docs/site/docs/providing-input/passing-parameters-to-components.md @@ -66,7 +66,7 @@ All of these examples do the same thing, here is what is going on: 1. The first example passes parameters using C# tuples, `(string name, object? value)`. 2. The second example also uses C# tuples to pass the parameters, but the name is retrieved in a refactor safe manner using the `nameof` keyword in C#. 3. The third example uses the factory method. -4. The last example uses the 's `Add` method, which takes a parameter selector expression that selects the parameter using a lambda, and forces you to provide the correct type for the value. This makes the builders methods strongly typed and refactor safe. +4. The last example uses the 's `Add` method, which takes a parameter selector expression that selects the parameter using a lambda, and forces you to provide the correct type for the value. This makes the builders methods strongly typed and refactor safe. # [Razor test code](#tab/razor) @@ -91,7 +91,7 @@ Using either C# or Razor test code, this can be done like this: These examples o the same thing, here is what is going on: 1. The first and second example uses the `EventCallback` factory method in (there are many overloads that take different kinds of `Action` and `Func` delegates), to pass a lambda as the event callback to the specified parameter. -2. The second example uses the 's `Add` method, which takes a parameter selector expression that selects the parameter using a lambda, and forces you to provide the correct type of callback method. This makes the builders methods strongly typed and refactor safe. +2. The second example uses the 's `Add` method, which takes a parameter selector expression that selects the parameter using a lambda, and forces you to provide the correct type of callback method. This makes the builders methods strongly typed and refactor safe. # [Razor test code](#tab/razor) @@ -118,7 +118,7 @@ The following subsections has different examples of child content being passed t These examples do the same thing, here is what is going on: 1. The first example uses the `ChildContent` factory method in , to pass a HTML markup string as the input to the `ChildContent` parameter. -2. The second example uses the 's `AddChildContent` method to pass a HTML markup string as the input to the `ChildContent` parameter. +2. The second example uses the 's `AddChildContent` method to pass a HTML markup string as the input to the `ChildContent` parameter. # [Razor test code](#tab/razor) @@ -139,7 +139,7 @@ To pass a component, e.g. the classic `` component, that does not take These examples do the same thing, here is what is going on: 1. The first example uses the `ChildContent` factory method in , where `TChildComponent` is the (child) component that should be passed to the component under test's `ChildContent` parameter. -2. The second example uses the 's `AddChildContent` method, where `TChildComponent` is the (child) component that should be passed to the component under test's `ChildContent` parameter. +2. The second example uses the 's `AddChildContent` method, where `TChildComponent` is the (child) component that should be passed to the component under test's `ChildContent` parameter. # [Razor test code](#tab/razor) @@ -162,7 +162,7 @@ To pass a component with parameters to a component under test, e.g. the ` These examples do the same thing, here is what is going on: 1. The first example uses the `ChildContent` factory method in , where `TChildComponent` is the (child) component that should be passed to the component under test. `ChildContent` factory method can take zero or more component parameters as input itself, which will be passed to the `TChildComponent` component, in this case, the `` component. -2. The second example uses the 's `AddChildContent` method, where `TChildComponent` is the (child) component that should be passed to the component under test. The `AddChildContent` method takes an optional as input, which can be used to pass parameters to the `TChildComponent` component, in this case, the `` component. +2. The second example uses the 's `AddChildContent` method, where `TChildComponent` is the (child) component that should be passed to the component under test. The `AddChildContent` method takes an optional as input, which can be used to pass parameters to the `TChildComponent` component, in this case, the `` component. # [Razor test code](#tab/razor) @@ -174,13 +174,13 @@ This is just regular Blazor child content parameter passing, where the `` #### Passing a mix of Razor and HTML to ChildContent Parameter -The easiest way to pass a mix of HTML markup and Razor markup to a `ChildContent` parameter is to use Razor based tests, as the example below illustrates. It is possible to do it in C# only tests, but that means writing `RenderTreeBuilder` code. +Some times you need to pass multiple different types of content to a ChildContent parameter, e.g. both some Markup and and a component. This can be done in the following way: # [C# test code](#tab/csharp) -[!code-csharp[](../../../samples/tests/xunit/AllKindsOfParamsTest.cs#L126-L139)] +[!code-csharp[](../../../samples/tests/xunit/AllKindsOfParamsTest.cs#L126-L146)] -Passing a mix of markup and a component to a `ChildContent` parameter is currently only possible using the , and unfortunately you have to create the render fragment manually using the `RenderTreeBuilder`, like this example demonstrates. +Passing a mix of markup and components to a `ChildContent` parameter is simply done by calling the 's `AddChildContent()` methods or using the `ChildContent()` factory methods in , as seen here. # [Razor test code](#tab/razor) @@ -198,18 +198,18 @@ In Blazor, a `RenderFragment` parameter can be regular HTML markup, it can be Ra The following subsections has different examples of content being passed to the following component's `RenderFragment` parameter: -[!code-csharp[RenderFragmentParams.razor](../../../samples/components/RenderFragmentParams.cs#L9-L14)] +[!code-csharp[RenderFragmentParams.razor](../../../samples/components/RenderFragmentParams.cs#L9-L13)] #### Passing HTML to a RenderFragment Parameter # [C# test code](#tab/csharp) -[!code-csharp[](../../../samples/tests/xunit/AllKindsOfParamsTest.cs#L145-L155)] +[!code-csharp[](../../../samples/tests/xunit/AllKindsOfParamsTest.cs#L152-L162)] These examples do the same thing, here is what is going on: 1. The first example uses the `RenderFragment` factory method in , to pass a HTML markup string as the input to the `RenderFragment` parameter. -2. The second example uses the 's `Add` method to pass a HTML markup string as the input to the `RenderFragment` parameter. +2. The second example uses the 's `Add` method to pass a HTML markup string as the input to the `RenderFragment` parameter. # [Razor test code](#tab/razor) @@ -225,12 +225,12 @@ To pass a component, e.g. the classic `` component, which does not take # [C# test code](#tab/csharp) -[!code-csharp[](../../../samples/tests/xunit/AllKindsOfParamsTest.cs#L161-L171)] +[!code-csharp[](../../../samples/tests/xunit/AllKindsOfParamsTest.cs#L168-L178)] These examples do the same thing, here is what is going on: -1. The first example uses the `Add` factory method in , where `TChildComponent` is the (child) component that should be passed to the `RenderFragment` parameter. -2. The second example uses the 's `Add` method, where `TChildComponent` is the (child) component that should be passed to the `RenderFragment` parameter. +1. The first example uses the `RenderFragment` factory method in , where `TChildComponent` is the (child) component that should be passed to the `RenderFragment` parameter. +2. The second example uses the 's `Add` method, where `TChildComponent` is the (child) component that should be passed to the `RenderFragment` parameter. # [Razor test code](#tab/razor) @@ -248,12 +248,12 @@ To pass a component with parameters to a `RenderFragment` parameter, e.g. the `< # [C# test code](#tab/csharp) -[!code-csharp[](../../../samples/tests/xunit/AllKindsOfParamsTest.cs#L177-L195)] +[!code-csharp[](../../../samples/tests/xunit/AllKindsOfParamsTest.cs#L184-L202)] These examples do the same thing, here is what is going on: 1. The first example uses the `RenderFragment` factory method in , where `TChildComponent` is the (child) component that should be passed to the `RenderFragment` parameter. `RenderFragment` factory method takes the name of the parameter and zero or more component parameters as input, which will be passed to the `TChildComponent` component, in this case, the `` component. -2. The second example uses the 's `Add` method, where `TChildComponent` is the (child) component that should be passed to the `RenderFragment` parameter. The `Add` method takes an optional as input, which can be used to pass parameters to the `TChildComponent` component, in this case, the `` component. +2. The second example uses the 's `Add` method, where `TChildComponent` is the (child) component that should be passed to the `RenderFragment` parameter. The `Add` method takes an optional as input, which can be used to pass parameters to the `TChildComponent` component, in this case, the `` component. # [Razor test code](#tab/razor) @@ -265,13 +265,13 @@ This is just regular Blazor `RenderFragment` parameter passing, where the `, and unfortunately you have to create the render fragment manually using the `RenderTreeBuilder`, like this example demonstrates. +Passing a mix of markup and components to a `RenderFragment` parameter is simply done by calling the 's `Add()` methods or using the `ChildContent()` factory methods in , as seen here. # [Razor test code](#tab/razor) @@ -295,12 +295,12 @@ To pass a template into a `RenderFragment` parameter, that just consists # [C# test code](#tab/csharp) -[!code-csharp[](../../../samples/tests/xunit/AllKindsOfParamsTest.cs#L220-L232)] +[!code-csharp[](../../../samples/tests/xunit/AllKindsOfParamsTest.cs#L234-L246)] These examples do the same thing, i.e. pass a HTML markup template into the component under test. This is done with the help of a `Func` delegate, that takes whatever the template value is as input, and returns a (markup) string. The delegate is automatically turned into a `RenderFragment` type and pass to the template parameter. 1. The first example passes data to the `Items` parameter, and then it uses the `Template` factory method in , that takes the name of the `RenderFragment` template parameter, and the `Func` delegate as input. -2. The second example uses the 's `Add` method to first add the data to `Items` parameter and then a `Func` delegate. +2. The second example uses the 's `Add` method to first add the data to `Items` parameter and then a `Func` delegate. The delegate creates a simple markup string in both examples. @@ -312,20 +312,17 @@ This is just regular Blazor `RenderFragment` parameter passing, in this *** -#### Passing HTML and Components based templates +#### Passing a Component-based template -To pass a template into a `RenderFragment` parameter, which consists of both regular HTML markup and components, in this case, the `` component listed below, do the following: +To pass a template into a `RenderFragment` parameter, which is based on a component which receives the template value as input, in this case, the `` component listed below, do the following: [!code-csharp[Item.razor](../../../samples/components/Item.razor)] # [C# test code](#tab/csharp) -[!code-csharp[](../../../samples/tests/xunit/AllKindsOfParamsTest.cs#L238-L266)] +[!code-csharp[](../../../samples/tests/xunit/AllKindsOfParamsTest.cs#L253-L269)] -These examples do the same thing, i.e. create a template which consist of a `
` element which wraps the `` component listed above. In both cases, must construct the `RenderFragemnt` type manually. Here is what is going on: - -1. The first example passes data to the `Items` parameter, and then it uses the `Template` factory method in , which takes the name of the `RenderFragment` template parameter and a `RenderFragment` type as input. -2. The second example uses the 's `Add` method to first add the data to `Items` parameter and then a `RenderFragment` type as input. +These examples do the same thing, i.e. create a template with the `` component listed above. # [Razor test code](#tab/razor) @@ -345,7 +342,7 @@ In the follow examples, we will pass a unmatched parameter to the following comp # [C# test code](#tab/csharp) -[!code-csharp[](../../../samples/tests/xunit/AllKindsOfParamsTest.cs#L272-L282)] +[!code-csharp[](../../../samples/tests/xunit/AllKindsOfParamsTest.cs#L275-L285)] These examples do the same thing, i.e. pass in the parameter `some-unknown-param` with the value `a value` to the component under test. @@ -371,13 +368,13 @@ To pass the unnamed `IsDarkTheme` cascading parameter to the `` # [C# test code](#tab/csharp) -[!code-csharp[](../../../samples/tests/xunit/AllKindsOfParamsTest.cs#L288-L304)] +[!code-csharp[](../../../samples/tests/xunit/AllKindsOfParamsTest.cs#L291-L307)] These examples do the same thing, i.e. pass in variable `isDarkTheme` to the cascading parameter `IsDarkTheme`. 1. The first example uses the `CascadingValue` factory method in to pass the unnamed parameter value. -2. The second example uses the `Add` method on the to pass the unnamed parameter value. -3. The last example uses the `Add` method on the with the parameter selector to explicitly select the desired cascading parameter and pass the unnamed parameter value that way. +2. The second example uses the `Add` method on the to pass the unnamed parameter value. +3. The last example uses the `Add` method on the with the parameter selector to explicitly select the desired cascading parameter and pass the unnamed parameter value that way. # [Razor test code](#tab/razor) @@ -393,12 +390,12 @@ To pass a named cascading parameter to the `` component, do the # [C# test code](#tab/csharp) -[!code-csharp[](../../../samples/tests/xunit/AllKindsOfParamsTest.cs#L310-L320)] +[!code-csharp[](../../../samples/tests/xunit/AllKindsOfParamsTest.cs#L313-L323)] These examples do the same thing, i.e. pass in value `Egil Hansen` to the cascading parameter with the name `LoggedInUser`. Note that the name of the parameter is not the same as the property of the parameter, e.g. `LoggedInUser` vs. `UserName`. 1. The first example uses the `CascadingValue` factory method in to pass the named parameter value, specifying the cascading parameters name and a value (not the property name). -2. The second example uses the `Add` method on the with the parameter selector to select the cascading parameter property and pass the parameter value that way. +2. The second example uses the `Add` method on the with the parameter selector to select the cascading parameter property and pass the parameter value that way. # [Razor test code](#tab/razor) @@ -414,13 +411,13 @@ To pass all cascading parameter to the `` component, do the fol # [C# test code](#tab/csharp) -[!code-csharp[](../../../samples/tests/xunit/AllKindsOfParamsTest.cs#L326-L348)] +[!code-csharp[](../../../samples/tests/xunit/AllKindsOfParamsTest.cs#L329-L351)] These examples do the same thing, i.e. pass both the unnamed `IsDarkTheme` cascading parameter, and the two named cascading parameters (`LoggedInUser`, `LoggedInEmail`). 1. The first example uses the `CascadingValue` factory method in to pass the unnamed and named parameter values. -2. The second example uses the `Add` method on the without a parameter to pass the unnamed parameter value, and `Add` method with the parameter selector to select each of the named parameters to pass the named parameter values. -3. The last example uses the `Add` method on the with the parameter selector to select both the named and unnamed cascading parameters and pass values to them that way. +2. The second example uses the `Add` method on the without a parameter to pass the unnamed parameter value, and `Add` method with the parameter selector to select each of the named parameters to pass the named parameter values. +3. The last example uses the `Add` method on the with the parameter selector to select both the named and unnamed cascading parameters and pass values to them that way. # [Razor test code](#tab/razor) diff --git a/docs/site/docs/test-doubles/faking-auth.md b/docs/site/docs/test-doubles/faking-auth.md index 51176375e..b23882a75 100644 --- a/docs/site/docs/test-doubles/faking-auth.md +++ b/docs/site/docs/test-doubles/faking-auth.md @@ -15,7 +15,7 @@ The test implementation of Blazor's authentication and authorization can be put - **Authenticated** and **authorized** - **Authenticated** and **authorized** with one or more **roles**, **claims**, and/or **policies** -bUnit's authentication and authorization implementation is easily available by calling [AddTestAuthorization()](xref:Bunit.FakeAuthorizationExtensions.AddTestAuthorization(Bunit.TestServiceProvider)) on a test context's `Services` collection. It returns an instance of the type that allows you to control the authentication and authorization state for a test. +bUnit's authentication and authorization implementation is easily available by calling [AddTestAuthorization()](xref:Bunit.TestDoubles.Authorization.FakeAuthorizationExtensions.AddTestAuthorization(Bunit.TestServiceProvider)) on a test context's `Services` collection. It returns an instance of the type that allows you to control the authentication and authorization state for a test. The following sections will show how to set each of these states in a test. diff --git a/docs/site/docs/verification/async-assertion.md b/docs/site/docs/verification/async-assertion.md index 2562dae8e..e08448a35 100644 --- a/docs/site/docs/verification/async-assertion.md +++ b/docs/site/docs/verification/async-assertion.md @@ -9,11 +9,11 @@ A test can fail if a component performs asynchronous renders, e.g. because it wa This happens because tests execute in the test framework's synchronization context and the test renderer executes renders in its own synchronization context. -bUnit comes with two methods that helps deal with this issue, the [`WaitForAssertion(Action, TimeSpan?)`](xref:Bunit.RenderedFragmentWaitForHelperExtensions.WaitForAssertion(Bunit.IRenderedFragmentBase,System.Action,System.Nullable{System.TimeSpan})) method covered on this page, and the [`WaitForState(Func, TimeSpan?)`](xref:Bunit.RenderedFragmentWaitForHelperExtensions.WaitForState(Bunit.IRenderedFragmentBase,System.Func{System.Boolean},System.Nullable{System.TimeSpan})) method covered on the page. +bUnit comes with two methods that helps deal with this issue, the `WaitForAssertion(Action, TimeSpan?)` method covered on this page, and the `WaitForState(Func, TimeSpan?)` method covered on the page. ## Waiting for Assertion to Pass Using `WaitForAssertion` -The [`WaitForAssertion(Action, TimeSpan?)`](xref:Bunit.RenderedFragmentWaitForHelperExtensions.WaitForAssertion(Bunit.IRenderedFragmentBase,System.Action,System.Nullable{System.TimeSpan})) method can be used to block and wait in a test method, until the provided assert action does not throw an exception, or the timeout is reached (the default timeout is one second). +The `WaitForAssertion(Action, TimeSpan?)` method can be used to block and wait in a test method, until the provided assert action does not throw an exception, or the timeout is reached (the default timeout is one second). > [!NOTE] > The `WaitForAssertion()` method will try the assert action pass to it when the `WaitForAssertion()` method is called, and every time the component under test renders. diff --git a/docs/site/docs/verification/verify-component-state.md b/docs/site/docs/verification/verify-component-state.md index f9b4f55f2..5690d48c9 100644 --- a/docs/site/docs/verification/verify-component-state.md +++ b/docs/site/docs/verification/verify-component-state.md @@ -5,7 +5,7 @@ title: Verifying the State of a Component Under Test # Verifying the State of a Component -Calling [`RenderComponent`()](xref:Bunit.TestContext.RenderComponent``1(System.Action{Bunit.ComponentParameterBuilder{``0}})) on a or calling on a returns an instance of the type. +Calling [`RenderComponent`()](xref:Bunit.RenderedComponentRenderExtensions.SetParametersAndRender``1(Bunit.IRenderedComponentBase{``0},Bunit.Rendering.ComponentParameter[])) on a or calling on a returns an instance of the type. The type makes it possible to inspect the instance of the component under test (`TComponent`), and trigger re-renders explicitly. @@ -29,7 +29,7 @@ Alert alert = cut.Instance; > [!WARNING] > While it is possible to set `[Parameter]` and `[CascadingParameter]` properties directly through the property on the type, doing so does not implicitly trigger a render and the component life-cycle methods are not called. > -> The correct approach is to set parameters through the [`SetParametersAndRender()`](xref:Bunit.IRenderedComponentBase`1.SetParametersAndRender(Bunit.Rendering.ComponentParameter[])) methods. See the page for more on this. +> The correct approach is to set parameters through the [`SetParametersAndRender()`](xref:Bunit.RenderedComponentRenderExtensions.SetParametersAndRender``1(Bunit.IRenderedComponentBase{``0},Bunit.Rendering.ComponentParameter[])) methods. See the page for more on this. ## Finding Components in the Render Tree diff --git a/src/Directory.Build.props b/src/Directory.Build.props index d2f6ec301..18a02a11a 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -18,10 +18,12 @@ netstandard2.1;net5.0 - 8.0 + 9.0 enable CS8600;CS8602;CS8603;CS8625 true + true + true diff --git a/src/bunit.core/Rendering/ComponentParameter.cs b/src/bunit.core/ComponentParameter.cs similarity index 92% rename from src/bunit.core/Rendering/ComponentParameter.cs rename to src/bunit.core/ComponentParameter.cs index 6c4849310..08a6fb017 100644 --- a/src/bunit.core/Rendering/ComponentParameter.cs +++ b/src/bunit.core/ComponentParameter.cs @@ -1,7 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; -namespace Bunit.Rendering +namespace Bunit { /// /// Represents a single parameter supplied to an @@ -31,7 +31,7 @@ namespace Bunit.Rendering /// An optional name /// An optional value /// Whether or not this is a cascading value - internal ComponentParameter(string? name, object? value, bool isCascadingValue) + 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"); @@ -76,7 +76,9 @@ public static implicit operator ComponentParameter((string? name, object? value, /// public bool Equals(ComponentParameter other) - => string.Equals(Name, other.Name, StringComparison.Ordinal) && Value == other.Value && IsCascadingValue == other.IsCascadingValue; + => string.Equals(Name, other.Name, StringComparison.Ordinal) + && (Value is null && other.Value is null || (Value?.Equals(other.Value) ?? false)) + && IsCascadingValue == other.IsCascadingValue; /// public override bool Equals(object? obj) => obj is ComponentParameter other && Equals(other); diff --git a/src/bunit.core/ComponentParameterBuilder.cs b/src/bunit.core/ComponentParameterBuilder.cs deleted file mode 100644 index 678588943..000000000 --- a/src/bunit.core/ComponentParameterBuilder.cs +++ /dev/null @@ -1,358 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Threading.Tasks; -using Bunit.Rendering; -using Microsoft.AspNetCore.Components; - -using EC = Microsoft.AspNetCore.Components.EventCallback; - -namespace Bunit -{ - /// - /// A builder to set a value for strongly typed ComponentParameters. - /// - /// The type of component under test to add the parameters - public sealed class ComponentParameterBuilder where TComponent : IComponent - { - private const string ParameterNameChildContent = "ChildContent"; - private static readonly PropertyInfo[] ComponentProperties = typeof(TComponent).GetProperties(); - private readonly List _componentParameters = new List(); - - /// - /// Adds an unmatched attribute to the component under test. - /// - /// The value to set for an unnamed cascading parameter - /// The value to set for an unnamed cascading parameter - /// A which can be chained - public ComponentParameterBuilder AddUnmatched(string key, object value) - { - var propertiesWithParameterAttributeAndCaptureUnmatchedValuesEqualsTrue = ComponentProperties - .Select(propertyInfo => propertyInfo.GetCustomAttribute()) - .Where(attribute => attribute is { } && attribute.CaptureUnmatchedValues) - .ToList(); - - if (!propertiesWithParameterAttributeAndCaptureUnmatchedValuesEqualsTrue.Any()) - throw new ArgumentException($"There is no public parameter with the attribute [Parameter(CaptureUnmatchedValues = true)] defined on the component '{typeof(TComponent)}'."); - - if (propertiesWithParameterAttributeAndCaptureUnmatchedValuesEqualsTrue.Count > 1) - throw new ArgumentException($"There are multiple public parameters with the attribute [Parameter(CaptureUnmatchedValues = true)] defined on the component '{typeof(TComponent)}'."); - - return AddParameterToList(key, value, false); - } - - /// - /// Add an unnamed cascading value for the component under test. - /// - /// The value to set for an unnamed cascading parameter - /// A which can be chained - public ComponentParameterBuilder Add(object value) - { - var propertiesWithCascadingParameterAttributeAndWithoutAName = ComponentProperties - .Select(propertyInfo => propertyInfo.GetCustomAttribute()) - .Where(attribute => attribute is { } && attribute.Name is null) - .ToList(); - - if (!propertiesWithCascadingParameterAttributeAndWithoutAName.Any()) - throw new ArgumentException($"There is no public parameter with the attribute [CascadingParameter()] defined on the component '{typeof(TComponent)}'."); - - if (propertiesWithCascadingParameterAttributeAndWithoutAName.Count > 1) - throw new ArgumentException($"There are multiple public parameters with the attribute [CascadingParameter()] defined on the component '{typeof(TComponent)}'."); - - return AddParameterToList(null, value, true); - } - - /// - /// Add a strongly typed parameter with a value for the component under test. - /// - /// The generic value type - /// The parameter selector which defines the parameter to add - /// The value, which cannot be null in case of cascading parameter - /// A which can be chained - public ComponentParameterBuilder Add(Expression> parameterSelector, [AllowNull] TValue value) - { - if (parameterSelector is null) - throw new ArgumentNullException(nameof(parameterSelector)); - - var (name, isCascading) = GetDetailsFromExpression(parameterSelector); - return AddParameterToList(name, value, isCascading); - } - - /// - /// Add a strongly typed parameter with a html markup value for the component under test. - /// - /// The parameter selector which defines the parameter to add - /// Markup to render as output - /// A which can be chained - public ComponentParameterBuilder Add(Expression> parameterSelector, string markup) - { - if (parameterSelector is null) - throw new ArgumentNullException(nameof(parameterSelector)); - - if (markup is null) - throw new ArgumentNullException(nameof(markup)); - - var (name, isCascading) = GetDetailsFromExpression(parameterSelector); - return AddParameterToList(name, markup.ToMarkupRenderFragment(), isCascading); - } - - /// - /// Add a strongly typed parameter with a template for the component under test. - /// - /// The generic value type - /// The parameter selector which defines the parameter to add - /// to pass to the parameter - /// A which can be chained - public ComponentParameterBuilder Add(Expression?>> parameterSelector, RenderFragment template) - { - if (parameterSelector is null) - throw new ArgumentNullException(nameof(parameterSelector)); - - if (template is null) - throw new ArgumentNullException(nameof(template)); - - var (name, isCascading) = GetDetailsFromExpression(parameterSelector); - return AddParameterToList(name, template, isCascading); - } - - /// - /// Add a strongly typed parameter with a markupFactory for the component under test. - /// - /// The generic value type - /// The parameter selector which defines the parameter to add - /// A markup factory that takes a as input and returns markup/HTML. - /// A which can be chained - public ComponentParameterBuilder Add(Expression?>> parameterSelector, Func markupFactory) - { - if (parameterSelector is null) - throw new ArgumentNullException(nameof(parameterSelector)); - - if (markupFactory is null) - throw new ArgumentNullException(nameof(markupFactory)); - - return Add(parameterSelector, value => renderTreeBuilder => renderTreeBuilder.AddMarkupContent(0, markupFactory(value))); - } - - /// - /// Add a strongly typed parameter with a callback for the component under test. - /// - /// The parameter selector which defines the parameter to add - /// The event callback that returns a . - /// A which can be chained - public ComponentParameterBuilder Add(Expression> parameterSelector, Func callback) - { - if (parameterSelector is null) - throw new ArgumentNullException(nameof(parameterSelector)); - - if (callback is null) - throw new ArgumentNullException(nameof(callback)); - - return Add(parameterSelector, EC.Factory.Create(this, callback)); - } - - /// - /// Add a strongly typed parameter with a action for the component under test. - /// - /// The parameter selector which defines the parameter to add - /// The event callback. - /// The . - public ComponentParameterBuilder Add(Expression> parameterSelector, Action callback) - { - if (parameterSelector is null) - throw new ArgumentNullException(nameof(parameterSelector)); - - if (callback is null) - throw new ArgumentNullException(nameof(callback)); - - return Add(parameterSelector, EC.Factory.Create(this, callback)); - } - - /// - /// Add a strongly typed parameter with a callback for the component under test. - /// - /// The parameter selector which defines the parameter to add - /// A callback that takes a and returns a . - /// A which can be chained - public ComponentParameterBuilder Add(Expression>> parameterSelector, Func callback) - { - if (parameterSelector is null) - throw new ArgumentNullException(nameof(parameterSelector)); - - if (callback is null) - throw new ArgumentNullException(nameof(callback)); - - var (name, isCascading) = GetDetailsFromExpression(parameterSelector); - return AddParameterToList(name, EC.Factory.Create(this, callback), isCascading); - } - - /// - /// Add a strongly typed parameter with a callback for the component under test. - /// - /// The parameter selector which defines the parameter to add - /// A callback that takes a . - /// A which can be chained - public ComponentParameterBuilder Add(Expression>> parameterSelector, Action callback) - { - if (parameterSelector is null) - throw new ArgumentNullException(nameof(parameterSelector)); - - if (callback is null) - throw new ArgumentNullException(nameof(callback)); - - var (name, isCascading) = GetDetailsFromExpression(parameterSelector); - return AddParameterToList(name, EC.Factory.Create(this, callback), isCascading); - } - - /// - /// Add a strongly typed parameter with a for the component under test. - /// - /// The parameter selector which defines the parameter to add - /// An optional builder action for the child component. - /// A which can be chained - public ComponentParameterBuilder Add(Expression> parameterSelector, Action>? childParameterBuilder = null) where TChildComponent : class, IComponent - { - if (parameterSelector is null) - throw new ArgumentNullException(nameof(parameterSelector)); - - var (name, isCascading) = GetDetailsFromExpression(parameterSelector); - - RenderFragment childContentFragment; - - if (childParameterBuilder is { }) - { - var build = new ComponentParameterBuilder(); - childParameterBuilder?.Invoke(build); - childContentFragment = build.Build().ToComponentRenderFragment(); - } - else - { - childContentFragment = Array.Empty().ToComponentRenderFragment(); - } - - return AddParameterToList(name, childContentFragment, isCascading); - } - - /// - /// Add a to build a ChildContent parameter. - /// - /// An optional builder action for the child component. - /// A which can be chained - public ComponentParameterBuilder AddChildContent(Action>? childParameterBuilder = null) where TChildComponent : class, IComponent - { - var (name, isCascading) = GetChildContentParameterDetails(); - - - RenderFragment childContentFragment; - - if (childParameterBuilder is { }) - { - var builder = new ComponentParameterBuilder(); - childParameterBuilder?.Invoke(builder); - childContentFragment = builder.Build().ToComponentRenderFragment(); - } - else - { - childContentFragment = Array.Empty().ToComponentRenderFragment(); - } - - return AddParameterToList(name, childContentFragment, isCascading); - } - - /// - /// Add a child component markup for a ChildContent parameter. - /// - /// Markup to render as output for the ChildContent parameter - /// A which can be chained - public ComponentParameterBuilder AddChildContent(string markup) - { - if (markup is null) - throw new ArgumentNullException(nameof(markup)); - - var (name, isCascading) = GetChildContentParameterDetails(); - return AddParameterToList(name, markup.ToMarkupRenderFragment(), isCascading); - } - - /// - /// Create a . - /// - /// A list of - public IReadOnlyList Build() - { - return _componentParameters; - } - - private static (string name, bool isCascading) GetChildContentParameterDetails() - { - var propertyInfo = typeof(TComponent).GetProperty(ParameterNameChildContent); - if (propertyInfo is null || propertyInfo.PropertyType != typeof(RenderFragment)) - throw new ArgumentException($"No public property with the name '{ParameterNameChildContent}' and type {typeof(RenderFragment).Name} is defined on the component '{typeof(TComponent)}'."); - - if (!TryGetDetailsFromPropertyInfo(propertyInfo, out var name, out var isCascading)) - throw new ArgumentException($"The public property with the name '{ParameterNameChildContent}' does not have the [Parameter] or [CascadingParameter] attribute defined in the component '{typeof(TComponent)}'."); - - return (name, isCascading); - } - - private static (string name, bool isCascading) GetDetailsFromExpression(Expression> parameterSelector) - { - if (parameterSelector.Body is MemberExpression memberExpression && memberExpression.Member is PropertyInfo propertyInfo) - { - if (!TryGetDetailsFromPropertyInfo(propertyInfo, out var name, out var isCascading)) - throw new ArgumentException($"The public property '{propertyInfo.Name}' selected by the provided '{parameterSelector}' does not have the [Parameter] or [CascadingParameter] attribute defined in the component '{typeof(TComponent)}'."); - - return (name, isCascading); - } - - throw new ArgumentException($"The parameterSelector '{parameterSelector}' does not resolve to a public property on the component '{typeof(TComponent)}'."); - } - - private static bool TryGetDetailsFromPropertyInfo(PropertyInfo propertyInfo, out string name, out bool isCascading) - { - var parameterAttribute = propertyInfo.GetCustomAttribute(); - if (parameterAttribute is { }) - { - // If ParameterAttribute is defined, get the name from the property and indicate that it's a normal property - name = propertyInfo.Name; - isCascading = false; - return true; - } - - var cascadingParameterAttribute = propertyInfo.GetCustomAttribute(); - if (cascadingParameterAttribute is { }) - { - if (!string.IsNullOrEmpty(cascadingParameterAttribute.Name)) - { - // The CascadingParameterAttribute is defined and has a valid name, get the defined - // name from this attribute and indicate that it's a cascading property - name = cascadingParameterAttribute.Name; - isCascading = true; - return true; - } - - // The CascadingParameterAttribute is defined, get the name from the property - // and indicate that it's a cascading property - name = propertyInfo.Name; - isCascading = true; - return true; - } - - // If both attributes are missing, return false - name = string.Empty; - isCascading = default; - return false; - } - - private ComponentParameterBuilder AddParameterToList(string? name, object? value, bool isCascading) - { - if (_componentParameters.Any(cp => cp.Name == name)) - throw new ArgumentException($"A parameter with the name '{name}' has already been added to the {typeof(TComponent).Name}."); - - _componentParameters.Add(new ComponentParameter(name, value, isCascading)); - - return this; - } - } -} diff --git a/src/bunit.core/ComponentParameterCollection.cs b/src/bunit.core/ComponentParameterCollection.cs new file mode 100644 index 000000000..c70bee4d8 --- /dev/null +++ b/src/bunit.core/ComponentParameterCollection.cs @@ -0,0 +1,272 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Bunit.Rendering; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Forms; +using Microsoft.AspNetCore.Components.Rendering; + +namespace Bunit +{ + /// + /// A collection for + /// + public class ComponentParameterCollection : ICollection, IReadOnlyCollection, IEnumerable + { + private static readonly MethodInfo CreateTemplateWrapperMethod = GetCreateTemplateWrapperMethod(); + private static readonly Type CascadingValueType = typeof(CascadingValue<>); + private List? _parameters; + + /// + /// Gets the number of in the collection. + /// + public int Count => _parameters?.Count ?? 0; + + /// + public bool IsReadOnly { get; } + + /// + /// Adds a to the collection. + /// + /// Parameter to add to the collection. + public void Add(ComponentParameter parameter) + { + if (parameter.Name is null && parameter.Value is null) + throw new ArgumentException("A component parameter without a name and value is not valid."); + + if (_parameters is null) + _parameters = new List(); + _parameters.Add(parameter); + } + + /// + /// Adds an enumerable of parameters to the collection. + /// + /// Parameters to add. + public void Add(IEnumerable parameters) + { + if (parameters is null) + throw new ArgumentNullException(nameof(parameters)); + + foreach (var cp in parameters) + { + Add(cp); + } + } + + /// + /// Checks if the is in the collection. + /// + /// Parameter to check with. + /// True if is in the collection, false otherwise. + public bool Contains(ComponentParameter parameter) => _parameters?.Contains(parameter) ?? false; + + /// + public void Clear() => _parameters?.Clear(); + + /// + public void CopyTo(ComponentParameter[] array, int arrayIndex) => _parameters?.CopyTo(array, arrayIndex); + + /// + public bool Remove(ComponentParameter item) => _parameters?.Remove(item) ?? false; + + /// + /// Creates a that will render a + /// component of type with + /// the parameters in the collection passed to it. + /// + /// Type of component to render. + public RenderFragment ToRenderFragment() where TComponent : IComponent + { + var cascadingValues = GetCascadingValues(); + + if (cascadingValues.Count > 0) + return AddCascadingValue; + else + return AddComponent; + + void AddCascadingValue(RenderTreeBuilder builder) + { + var cv = cascadingValues.Dequeue(); + + builder.OpenComponent(0, cv.Type); + + if (cv.Parameter.Name is string) + builder.AddAttribute(1, nameof(CascadingValue.Name), cv.Parameter.Name); + + builder.AddAttribute(2, nameof(CascadingValue.Value), cv.Parameter.Value); + builder.AddAttribute(3, nameof(CascadingValue.IsFixed), true); + + if (cascadingValues.Count > 0) + builder.AddAttribute(4, nameof(CascadingValue.ChildContent), (RenderFragment)(AddCascadingValue)); + else + builder.AddAttribute(4, nameof(CascadingValue.ChildContent), (RenderFragment)(AddComponent)); + + builder.CloseComponent(); + } + + void AddComponent(RenderTreeBuilder builder) + { + builder.OpenComponent(0); + AddAttributes(builder); + builder.CloseComponent(); + } + + void AddAttributes(RenderTreeBuilder builder) + { + if (_parameters is null) return; + + var attrCount = 100; + + foreach (var pgroup in _parameters.Where(x => !x.IsCascadingValue).GroupBy(x => x.Name)) + { + var group = pgroup.ToArray(); + var groupObject = group.FirstOrDefault(x => !(x.Value is null)).Value; + + if (group.Length == 1) + { + var p = group[0]; + builder.AddAttribute( + attrCount++, + p.Name!, // BANG: ComponentParameter does not allow a regular param to be created without a name + p.Value + ); + + continue; + } + + if (groupObject is RenderFragment) + { + builder.AddAttribute( + attrCount++, + group[0].Name!, // BANG: ComponentParameter does not allow a regular param to be created without a name + (RenderFragment)(ccBuilder => + { + for (int i = 0; i < group.Length; i++) + { + if (group[i].Value is RenderFragment rf) + ccBuilder.AddContent(i, rf); + } + }) + ); + + continue; + } + + var groupType = groupObject?.GetType(); + + if (groupType != null && groupType.IsGenericType && groupType.GetGenericTypeDefinition() == typeof(RenderFragment<>)) + { + builder.AddAttribute( + attrCount++, + group[0].Name!, // BANG: ComponentParameter does not allow a regular param to be created without a name + WrapTemplates(groupType, group) + ); + + continue; + } + + throw new ArgumentException($"The parameter with the name '{pgroup.Key}' was added more than once. This parameter can only be added one time."); + } + } + + Queue<(ComponentParameter Parameter, Type Type)> GetCascadingValues() + { + var cascadingValues = _parameters?.Where(x => x.IsCascadingValue) + .Select(x => (Parameter: x, Type: GetCascadingValueType(x))) + .ToArray() ?? Array.Empty<(ComponentParameter Parameter, Type Type)>(); + + // Detect duplicated unnamed values + for (int i = 0; i < cascadingValues.Length; i++) + { + + for (int j = i + 1; j < cascadingValues.Length; j++) + { + if (cascadingValues[i].Type == cascadingValues[j].Type) + { + var iName = cascadingValues[i].Parameter.Name; + if (iName is null) + { + var cascadingValueType = cascadingValues[i].Type.GetGenericArguments()[0]; + throw new ArgumentException($"Two or more unnamed cascading values with the type '{cascadingValueType.Name}' was added. " + + $"Only add one unnamed cascading value of the same type."); + } + + if (iName.Equals(cascadingValues[j].Parameter.Name, StringComparison.Ordinal)) + { + throw new ArgumentException($"Two or more named cascading values with the name '{iName}' and the same type was added. " + + $"Only add one named cascading value with the same name and type."); + } + } + } + } + + return new Queue<(ComponentParameter Parameter, Type Type)>(cascadingValues); + } + } + + /// + public IEnumerator GetEnumerator() + { + if (_parameters is null) + { + yield break; + } + else + { + for (int i = 0; i < _parameters.Count; i++) + { + yield return _parameters[i]; + } + } + } + + /// + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + + private static object WrapTemplates(Type templateParamterType, ComponentParameter[] templateParameters) + { + // gets the generic argument to RenderFragment<>, e.g. string with RenderFragment + var templateType = templateParamterType.GetGenericArguments()[0]; + + // this creates an invokable version of CreateTemplateWrapper with the + // generic type set to tmeplateType, e.g. CreateTemplateWrapper + var templateWrapper = CreateTemplateWrapperMethod.MakeGenericMethod(templateType); + + // BANG: since CreateTemplateWrapper will never return null BANG (!) is safe here + return templateWrapper.Invoke(null, new object[] { templateParameters })!; + } + + private static RenderFragment CreateTemplateWrapper(ComponentParameter[] subTemplateParams) + { + return input => builder => + { + foreach (var tp in subTemplateParams) + { + if (tp.Value is RenderFragment rf) + builder.AddContent(0, rf(input)); + else + throw new ArgumentException($"The parameter with name {tp.Name} was different types of templates.", tp.Name); + } + }; + } + + private static Type GetCascadingValueType(ComponentParameter parameter) + { + if (parameter.Value is null) + throw new InvalidOperationException("Cannot get the type of a null object"); + var cascadingValueType = parameter.Value.GetType(); + return CascadingValueType.MakeGenericType(cascadingValueType); + } + + private static MethodInfo GetCreateTemplateWrapperMethod() + { + var result = typeof(ComponentParameterCollection).GetMethod(nameof(CreateTemplateWrapper), BindingFlags.NonPublic | BindingFlags.Static); + return result ?? throw new InvalidOperationException($"Could not find the {nameof(CreateTemplateWrapper)} method."); + } + } +} diff --git a/src/bunit.core/ComponentParameterCollectionBuilder.cs b/src/bunit.core/ComponentParameterCollectionBuilder.cs new file mode 100644 index 000000000..58162af94 --- /dev/null +++ b/src/bunit.core/ComponentParameterCollectionBuilder.cs @@ -0,0 +1,351 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading.Tasks; +using Bunit.Rendering; +using Microsoft.AspNetCore.Components; + +namespace Bunit +{ + /// + /// A builder for a specific component under test. + /// + /// The type of component under test to add the parameters + public sealed class ComponentParameterCollectionBuilder where TComponent : IComponent + { + private const string ChildContent = nameof(ChildContent); + + /// + /// Gets whether TComponent has a [Parameter(CaptureUnmatchedValues = true)] parameter. + /// + private static bool HasUnmatchedCaptureParameter { get; } + = typeof(TComponent).GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Select(x => x.GetCustomAttribute()) + .OfType() + .Any(x => x.CaptureUnmatchedValues); + + private readonly ComponentParameterCollection _parameters = new ComponentParameterCollection(); + + /// + /// Creates an instance of the . + /// + public ComponentParameterCollectionBuilder() { } + + /// + /// Creates an instance of the and + /// invokes the with it as the argument. + /// + public ComponentParameterCollectionBuilder(Action>? parameterAdder) + { + parameterAdder?.Invoke(this); + } + + /// + /// Adds a component parameter for the parameter selected with + /// with the value . + /// + /// Type of . + /// A lambda function that selects the parameter. + /// The value to pass to . + /// This . + public ComponentParameterCollectionBuilder Add(Expression> parameterSelector, [AllowNull] TValue value) + { + var (name, cascadingValueName, isCascading) = GetParameterInfo(parameterSelector); + return isCascading + ? AddCascadingValueParameter(cascadingValueName, value) + : AddParameter(name, value); + } + + /// + /// Adds a component parameter for a parameter selected with , + /// where the value is created through the argument. + /// + /// The type of component to create a for. + /// A lambda function that selects the parameter. + /// A parameter builder for the . + /// This . + public ComponentParameterCollectionBuilder Add(Expression> parameterSelector, Action>? childParameterBuilder = null) + where TChildComponent : IComponent => Add(parameterSelector, GetRenderFragment(childParameterBuilder)); + + /// + /// Adds a component parameter for a parameter selected with , + /// where the value is the markup passed in through the argument. + /// + /// A lambda function that selects the parameter. + /// The markup string to pass to the . + /// This . + public ComponentParameterCollectionBuilder Add(Expression> parameterSelector, string markup) + => Add(parameterSelector, markup.ToMarkupRenderFragment()); + + /// + /// Adds a component parameter for a template parameter selected with , + /// where the template is based on the argument. + /// + /// The context type of the . + /// A lambda function that selects the parameter. + /// A markup factory used to create the template with. + /// This . + public ComponentParameterCollectionBuilder Add(Expression?>> parameterSelector, Func markupFactory) + { + if (markupFactory is null) throw new ArgumentNullException(nameof(markupFactory)); + return Add(parameterSelector, v => b => b.AddMarkupContent(0, markupFactory(v))); + } + + /// + /// Adds a component parameter for a template parameter selected with , + /// where the template is based on the , which is used + /// to create a that renders a inside the template. + /// + /// The type of component to create a for. + /// The context type of the . + /// A lambda function that selects the parameter. + /// A template factory used to create the parameters being passed to the . + /// This . + public ComponentParameterCollectionBuilder Add(Expression?>> parameterSelector, Func>> templateFactory) + where TChildComponent : IComponent + { + if (templateFactory is null) throw new ArgumentNullException(nameof(templateFactory)); + return Add(parameterSelector, value => GetRenderFragment(templateFactory(value))); + } + + /// + /// Adds a component parameter for an parameter selected with , + /// where the is used as value. + /// + /// A lambda function that selects the parameter. + /// The callback to pass to the . + /// This . + public ComponentParameterCollectionBuilder Add(Expression> parameterSelector, Action callback) + => Add(parameterSelector, EventCallback.Factory.Create(callback?.Target!, callback!)); + + /// + /// Adds a component parameter for a nullable parameter selected with , + /// where the is used as value. + /// + /// A lambda function that selects the parameter. + /// The callback to pass to the . + /// This . + public ComponentParameterCollectionBuilder Add(Expression> parameterSelector, Action callback) + => Add(parameterSelector, EventCallback.Factory.Create(callback?.Target!, callback!)); + + /// + /// Adds a component parameter for an parameter selected with , + /// where the is used as value. + /// + /// A lambda function that selects the parameter. + /// The callback to pass to the . + /// This . + public ComponentParameterCollectionBuilder Add(Expression> parameterSelector, Action callback) + => Add(parameterSelector, EventCallback.Factory.Create(callback?.Target!, callback!)); + + /// + /// Adds a component parameter for a nullable parameter selected with , + /// where the is used as value. + /// + /// A lambda function that selects the parameter. + /// The callback to pass to the . + /// This . + public ComponentParameterCollectionBuilder Add(Expression> parameterSelector, Action callback) + => Add(parameterSelector, EventCallback.Factory.Create(callback?.Target!, callback!)); + + /// + /// Adds a component parameter for an parameter selected with , + /// where the is used as value. + /// + /// A lambda function that selects the parameter. + /// The callback to pass to the . + /// This . + public ComponentParameterCollectionBuilder Add(Expression> parameterSelector, Func callback) + => Add(parameterSelector, EventCallback.Factory.Create(callback?.Target!, callback!)); + + /// + /// Adds a component parameter for a nullable parameter selected with , + /// where the is used as value. + /// + /// A lambda function that selects the parameter. + /// The callback to pass to the . + /// This . + public ComponentParameterCollectionBuilder Add(Expression> parameterSelector, Func callback) + => Add(parameterSelector, EventCallback.Factory.Create(callback?.Target!, callback!)); + + /// + /// Adds a component parameter for an parameter selected with , + /// where the is used as value. + /// + /// A lambda function that selects the parameter. + /// The callback to pass to the . + /// This . + public ComponentParameterCollectionBuilder Add(Expression>> parameterSelector, Action callback) + => Add(parameterSelector, EventCallback.Factory.Create(callback?.Target!, callback!)); + + /// + /// Adds a component parameter for a nullable parameter selected with , + /// where the is used as value. + /// + /// A lambda function that selects the parameter. + /// The callback to pass to the . + /// This . + public ComponentParameterCollectionBuilder Add(Expression?>> parameterSelector, Action callback) + => Add(parameterSelector, EventCallback.Factory.Create(callback?.Target!, callback!)); + + /// + /// Adds a component parameter for an parameter selected with , + /// where the is used as value. + /// + /// A lambda function that selects the parameter. + /// The callback to pass to the . + /// This . + public ComponentParameterCollectionBuilder Add(Expression>> parameterSelector, Action callback) + => Add(parameterSelector, EventCallback.Factory.Create(callback?.Target!, callback!)); + + /// + /// Adds a component parameter for a nullable parameter selected with , + /// where the is used as value. + /// + /// A lambda function that selects the parameter. + /// The callback to pass to the . + /// This . + public ComponentParameterCollectionBuilder Add(Expression?>> parameterSelector, Action callback) + => Add(parameterSelector, EventCallback.Factory.Create(callback?.Target!, callback!)); + + /// + /// Adds a component parameter for an parameter selected with , + /// where the is used as value. + /// + /// A lambda function that selects the parameter. + /// The callback to pass to the . + /// This . + public ComponentParameterCollectionBuilder Add(Expression>> parameterSelector, Func callback) + => Add(parameterSelector, EventCallback.Factory.Create(callback?.Target!, callback!)); + + /// + /// Adds a component parameter for a nullable parameter selected with , + /// where the is used as value. + /// + /// A lambda function that selects the parameter. + /// The callback to pass to the . + /// This . + public ComponentParameterCollectionBuilder Add(Expression?>> parameterSelector, Func callback) + => Add(parameterSelector, EventCallback.Factory.Create(callback?.Target!, callback!)); + + /// + /// Adds a ChildContent type parameter with the as value. + /// + /// Note, this is equivalent to Add(p => p.ChildContent, childContent). + /// + /// The to pass the ChildContent parameter. + /// This . + public ComponentParameterCollectionBuilder AddChildContent(RenderFragment childContent) + { + if (!HasChildContentParameter()) + throw new ArgumentException($"The component '{typeof(TComponent)}' does not have a {ChildContent} [Parameter] attribute."); + + return AddParameter(ChildContent, childContent); + } + + /// + /// Adds a ChildContent type parameter with the as value + /// wrapped in a . + /// + /// Note, this is equivalent to Add(p => p.ChildContent, "..."). + /// + /// The markup string to pass the ChildContent parameter wrapped in a . + /// This . + public ComponentParameterCollectionBuilder AddChildContent(string markup) + => AddChildContent(markup.ToMarkupRenderFragment()); + + /// + /// Adds a ChildContent type parameter, that is passed a , + /// which will render the with the parameters passed to . + /// + /// Type of child component to pass to the ChildContent parameter. + /// A parameter builder for the . + /// This . + public ComponentParameterCollectionBuilder AddChildContent(Action>? childParameterBuilder = null) where TChildComponent : IComponent + => AddChildContent(GetRenderFragment(childParameterBuilder)); + + /// + /// Adds an UNNAMED cascading value around the when it is rendered. Used to + /// pass cascading values to child components of . + /// + /// The type of cascading value. + /// The cascading value. + /// This . + public ComponentParameterCollectionBuilder AddCascadingValue(TValue cascadingValue) where TValue : notnull + => AddCascadingValueParameter(null, cascadingValue); + + /// + /// Adds an NAMED cascading value around the when it is rendered. Used to + /// pass cascading values to child components of . + /// + /// The type of cascading value. + /// The name of the cascading value. + /// The cascading value. + /// This . + public ComponentParameterCollectionBuilder AddCascadingValue(string name, TValue cascadingValue) where TValue : notnull + => AddCascadingValueParameter(name, cascadingValue); + + /// + /// Adds an unmatched attribute value to . + /// + /// The name of the unmatched attribute. + /// The value of the unmatched attribute. + /// This . + public ComponentParameterCollectionBuilder AddUnmatched(string name, object? value = null) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("An unmatched parameter (attribute) cannot have an empty name.", nameof(name)); + + if (!HasUnmatchedCaptureParameter) + throw new ArgumentException($"The component '{typeof(TComponent)}' does not have an [Parameter(CaptureUnmatchedValues = true)] parameter."); + + return AddParameter(name, value); + } + + /// + /// Builds the . + /// + public ComponentParameterCollection Build() => _parameters; + + private static (string paramName, string? cascadingValueName, bool isCascading) GetParameterInfo(Expression> parameterSelector) + { + if (parameterSelector is null) throw new ArgumentNullException(nameof(parameterSelector)); + + if (!(parameterSelector.Body is MemberExpression memberExpression) || !(memberExpression.Member is PropertyInfo propertyInfo)) + throw new ArgumentException($"The parameter selector '{parameterSelector}' does not resolve to a public property on the component '{typeof(TComponent)}'."); + + var paramAttr = propertyInfo.GetCustomAttribute(inherit: false); + var cascadingParamAttr = propertyInfo.GetCustomAttribute(inherit: false); + + if (paramAttr is null && cascadingParamAttr is null) + throw new ArgumentException($"The parameter selector '{parameterSelector}' does not resolve to a public property on the component '{typeof(TComponent)}' with a [Parameter] or [CascadingParameter] attribute."); + + return (propertyInfo.Name, cascadingParamAttr?.Name, cascadingParamAttr is not null); + } + + private static bool HasChildContentParameter() + => typeof(TComponent).GetProperty(ChildContent, BindingFlags.Public | BindingFlags.Instance) is PropertyInfo ccProp + && ccProp.GetCustomAttribute(inherit: false) != null; + + private ComponentParameterCollectionBuilder AddParameter(string name, [AllowNull] TValue value) + { + _parameters.Add(ComponentParameter.CreateParameter(name, value)); + return this; + } + + private ComponentParameterCollectionBuilder AddCascadingValueParameter(string? name, object? cascadingValue) + { + var value = cascadingValue ?? throw new ArgumentNullException(nameof(cascadingValue), "Passing null values to cascading value parameters is not allowed."); + _parameters.Add(ComponentParameter.CreateCascadingValue(name, value)); + return this; + } + + private static RenderFragment GetRenderFragment(Action>? childParameterBuilder) where TChildComponent : IComponent + { + var childBuilder = new ComponentParameterCollectionBuilder(childParameterBuilder); + return childBuilder.Build().ToRenderFragment(); + } + } +} diff --git a/src/bunit.core/ComponentParameterFactory.cs b/src/bunit.core/ComponentParameterFactory.cs index 8b1f66a5f..210b1246f 100644 --- a/src/bunit.core/ComponentParameterFactory.cs +++ b/src/bunit.core/ComponentParameterFactory.cs @@ -154,6 +154,17 @@ public static ComponentParameter ChildContent(params ComponentParame return RenderFragment(nameof(ChildContent), parameters); } + /// + /// Creates a ChildContent parameter that will pass the provided + /// to the parameter in the component. + /// + /// The to pass to the ChildContent parameter. + /// The . + public static ComponentParameter ChildContent(RenderFragment renderFragment) + { + return Parameter(nameof(ChildContent), renderFragment); + } + /// /// Creates a with the provided /// as rendered output and passes it to the parameter specified in . @@ -176,7 +187,8 @@ public static ComponentParameter RenderFragment(string name, string markup) /// The . public static ComponentParameter RenderFragment(string name, params ComponentParameter[] parameters) where TComponent : class, IComponent { - return ComponentParameter.CreateParameter(name, parameters.ToComponentRenderFragment()); + var cpc = new ComponentParameterCollection() { parameters }; + return ComponentParameter.CreateParameter(name, cpc.ToRenderFragment()); } /// @@ -205,5 +217,25 @@ public static ComponentParameter Template(string name, Func(name, value => (RenderTreeBuilder builder) => builder.AddMarkupContent(0, markupFactory(value))); } + + /// + /// Creates a template component parameter which will pass the a + /// to the at runtime. The parameters returned from it + /// will be passed to the and it will be rendered as the template. + /// + /// The type of component to render in template. + /// The value used to build the content. + /// Parameter name. + /// The parameter collection builder function that will be passed the template . + /// The . + public static ComponentParameter Template(string name, Func parameterCollectionBuilder) + where TComponent : IComponent + { + return Template(name, value => + { + var cpc = new ComponentParameterCollection() { parameterCollectionBuilder(value) }; + return cpc.ToRenderFragment(); + }); + } } } diff --git a/src/bunit.core/Extensions/BlazorExtensions.cs b/src/bunit.core/Extensions/BlazorExtensions.cs index 4766fd912..cf69f18c9 100644 --- a/src/bunit.core/Extensions/BlazorExtensions.cs +++ b/src/bunit.core/Extensions/BlazorExtensions.cs @@ -12,9 +12,12 @@ public static class BlazorExtensions /// /// Markup to render /// The . - public static RenderFragment ToMarkupRenderFragment(this string markup) + public static RenderFragment ToMarkupRenderFragment(this string? markup) { - return builder => builder.AddMarkupContent(0, markup); + if (string.IsNullOrEmpty(markup)) + return builder => { }; + return + builder => builder.AddMarkupContent(0, markup); } } } diff --git a/src/bunit.core/Extensions/ComponentParameterExtensions.cs b/src/bunit.core/Extensions/ComponentParameterExtensions.cs deleted file mode 100644 index 08b69e4e3..000000000 --- a/src/bunit.core/Extensions/ComponentParameterExtensions.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Bunit.Rendering; -using Microsoft.AspNetCore.Components; - -namespace Bunit -{ - /// - /// Helpful extensions for working with and collections of these. - /// - public static class ComponentParameterExtensions - { - /// - /// Creates a that will render a component of type, - /// with the provided . If one or more of the include - /// a cascading values, the will be wrapped in - /// components. - /// - /// Type of component to render in the render fragment - /// Parameters to pass to the component - /// The . - public static RenderFragment ToComponentRenderFragment(this IEnumerable parameters) where TComponent : IComponent - { - var parametersList = parameters as IReadOnlyList ?? parameters.ToArray(); - var cascadingParams = new Queue(parametersList.Where(x => x.IsCascadingValue)); - - if (cascadingParams.Count > 0) - return CreateCascadingValueRenderFragment(cascadingParams, parametersList); - else - return CreateComponentRenderFragment(parametersList); - - static RenderFragment CreateCascadingValueRenderFragment(Queue cascadingParams, IReadOnlyList parameters) - { - var cp = cascadingParams.Dequeue(); - var cascadingValueType = GetCascadingValueType(cp); - return builder => - { - builder.OpenComponent(0, cascadingValueType); - if (cp.Name is { }) - builder.AddAttribute(1, nameof(CascadingValue.Name), cp.Name); - - builder.AddAttribute(2, nameof(CascadingValue.Value), cp.Value); - builder.AddAttribute(3, nameof(CascadingValue.IsFixed), true); - - if (cascadingParams.Count > 0) - builder.AddAttribute(4, nameof(CascadingValue.ChildContent), CreateCascadingValueRenderFragment(cascadingParams, parameters)); - else - builder.AddAttribute(4, nameof(CascadingValue.ChildContent), CreateComponentRenderFragment(parameters)); - - builder.CloseComponent(); - }; - } - - static RenderFragment CreateComponentRenderFragment(IReadOnlyList parameters) - { - return builder => - { - builder.OpenComponent(0, typeof(TComponent)); - - for (var i = 0; i < parameters.Count; i++) - { - var para = parameters[i]; - if (!para.IsCascadingValue && para.Name is { }) - builder.AddAttribute(i + 1, para.Name, para.Value); - } - - builder.CloseComponent(); - }; - } - } - - private static readonly Type CascadingValueType = typeof(CascadingValue<>); - - private static Type GetCascadingValueType(ComponentParameter parameter) - { - if (parameter.Value is null) - throw new InvalidOperationException("Cannot get the type of a null object"); - var cascadingValueType = parameter.Value.GetType(); - return CascadingValueType.MakeGenericType(cascadingValueType); - } - } -} diff --git a/src/bunit.core/Extensions/RenderedComponentRenderExtensions.cs b/src/bunit.core/Extensions/RenderedComponentRenderExtensions.cs index 35f283acd..1c49217b4 100644 --- a/src/bunit.core/Extensions/RenderedComponentRenderExtensions.cs +++ b/src/bunit.core/Extensions/RenderedComponentRenderExtensions.cs @@ -50,8 +50,8 @@ public static void SetParametersAndRender(this IRenderedComponentBas /// Render the component under test again with the provided parameters from the . /// /// The rendered component to re-render with new parameters - /// An action that receives a . - public static void SetParametersAndRender(this IRenderedComponentBase renderedComponent, Action> parameterBuilder) + /// An action that receives a . + public static void SetParametersAndRender(this IRenderedComponentBase renderedComponent, Action> parameterBuilder) where TComponent : IComponent { if (renderedComponent is null) @@ -59,14 +59,11 @@ public static void SetParametersAndRender(this IRenderedComponentBas if (parameterBuilder is null) throw new ArgumentNullException(nameof(parameterBuilder)); - var builder = new ComponentParameterBuilder(); - parameterBuilder(builder); - + var builder = new ComponentParameterCollectionBuilder(parameterBuilder); SetParametersAndRender(renderedComponent, ToParameterView(builder.Build())); } - - private static ParameterView ToParameterView(IReadOnlyList parameters) + private static ParameterView ToParameterView(IReadOnlyCollection parameters) { var parameterView = ParameterView.Empty; if (parameters.Count > 0) diff --git a/src/bunit.core/Extensions/WaitForHelpers/RenderedFragmentWaitForHelperExtensions.cs b/src/bunit.core/Extensions/WaitForHelpers/RenderedFragmentWaitForHelperExtensions.cs index 0c6347125..c2dcce3a9 100644 --- a/src/bunit.core/Extensions/WaitForHelpers/RenderedFragmentWaitForHelperExtensions.cs +++ b/src/bunit.core/Extensions/WaitForHelpers/RenderedFragmentWaitForHelperExtensions.cs @@ -26,7 +26,7 @@ public static void WaitForState(this IRenderedFragmentBase renderedFragment, Fun { waiter.WaitTask.Wait(); } - catch (AggregateException e) when (e.InnerException is { }) + catch (AggregateException e) when (e.InnerException is not null) { throw e.InnerException; } @@ -49,7 +49,7 @@ public static void WaitForAssertion(this IRenderedFragmentBase renderedFragment, { waiter.WaitTask.Wait(); } - catch (AggregateException e) when (e.InnerException is { }) + catch (AggregateException e) when (e.InnerException is not null) { throw e.InnerException; } diff --git a/src/bunit.core/RazorTesting/FixtureBase.cs b/src/bunit.core/RazorTesting/FixtureBase.cs index 7362802ad..f2ff8a6e2 100644 --- a/src/bunit.core/RazorTesting/FixtureBase.cs +++ b/src/bunit.core/RazorTesting/FixtureBase.cs @@ -67,7 +67,7 @@ public override void Validate() throw new ArgumentException($"No '{nameof(ChildContent)}' specified in the {GetType().Name} component.", nameof(ChildContent)); if (Test is null && TestAsync is null) throw new ArgumentException($"No test action provided via the '{nameof(Test)}' or '{nameof(TestAsync)}' parameters to the {GetType().Name} component.", nameof(Test)); - if (Test is { } && TestAsync is { }) + if (Test is not null && TestAsync is not null) throw new ArgumentException($"Only one of the '{nameof(Test)}' or '{nameof(TestAsync)}' actions can be provided to the {GetType().Name} component at the same time.", nameof(Test)); } @@ -75,16 +75,16 @@ public override void Validate() protected virtual async Task Run(TFixture self) { Validate(); - if (Setup is { }) + if (Setup is not null) TryRun(Setup, self); - if (SetupAsync is { }) + if (SetupAsync is not null) await TryRunAsync(SetupAsync, self).ConfigureAwait(false); - if (Test is { }) + if (Test is not null) TryRun(Test, self); - if (TestAsync is { }) + if (TestAsync is not null) await TryRunAsync(TestAsync, self).ConfigureAwait(false); } } diff --git a/src/bunit.core/Rendering/ITestRenderer.cs b/src/bunit.core/Rendering/ITestRenderer.cs index 56a896887..fe38a2b49 100644 --- a/src/bunit.core/Rendering/ITestRenderer.cs +++ b/src/bunit.core/Rendering/ITestRenderer.cs @@ -33,12 +33,12 @@ public interface ITestRenderer IRenderedFragmentBase RenderFragment(RenderFragment renderFragment); /// - /// Renders a with the parameters passed to it. + /// Renders a with the passed to it. /// /// The type of component to render. - /// The parameters to pass to the component. + /// The parameters to pass to the component. /// A that provides access to the rendered component. - IRenderedComponentBase RenderComponent(IEnumerable componentParameters) + IRenderedComponentBase RenderComponent(ComponentParameterCollection parameters) where TComponent : IComponent; /// diff --git a/src/bunit.core/Rendering/TestComponentRenderer.cs b/src/bunit.core/Rendering/TestComponentRenderer.cs index 2a641f3a0..c211eeb4d 100644 --- a/src/bunit.core/Rendering/TestComponentRenderer.cs +++ b/src/bunit.core/Rendering/TestComponentRenderer.cs @@ -90,7 +90,7 @@ private IReadOnlyList GetRazorTests(int fromComponentId) private void AssertNoUnhandledExceptions() { - if (_unhandledException is { } unhandled) + if (_unhandledException is Exception unhandled) { _unhandledException = null; ExceptionDispatchInfo.Capture(unhandled).Throw(); diff --git a/src/bunit.core/Rendering/TestRenderer.cs b/src/bunit.core/Rendering/TestRenderer.cs index fdc7b2241..c28a13d7a 100644 --- a/src/bunit.core/Rendering/TestRenderer.cs +++ b/src/bunit.core/Rendering/TestRenderer.cs @@ -39,18 +39,19 @@ public IRenderedFragmentBase RenderFragment(RenderFragment renderFragment) } /// - public IRenderedComponentBase RenderComponent(IEnumerable parameters) + public IRenderedComponentBase RenderComponent(ComponentParameterCollection parameters) where TComponent : IComponent { - var fragment = parameters.ToComponentRenderFragment(); - return Render(fragment, id => _activator.CreateRenderedComponent(id)); + if (parameters is null) throw new ArgumentNullException(nameof(parameters)); + + var renderFragment = parameters.ToRenderFragment(); + return Render(renderFragment, id => _activator.CreateRenderedComponent(id)); } /// public new Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo fieldInfo, EventArgs eventArgs) { - if (fieldInfo is null) - throw new ArgumentNullException(nameof(fieldInfo)); + if (fieldInfo is null) throw new ArgumentNullException(nameof(fieldInfo)); var result = Dispatcher.InvokeAsync(() => base.DispatchEventAsync(eventHandlerId, fieldInfo, eventArgs)); @@ -265,7 +266,7 @@ private ArrayRange GetOrLoadRenderTreeFrame(RenderTreeFrameColl private void AssertNoUnhandledExceptions() { - if (_unhandledException is { } unhandled) + if (_unhandledException is Exception unhandled) { _unhandledException = null; LogUnhandledException(unhandled); diff --git a/src/bunit.core/TestContextBase.cs b/src/bunit.core/TestContextBase.cs index 23d182cb4..8243c6fdb 100644 --- a/src/bunit.core/TestContextBase.cs +++ b/src/bunit.core/TestContextBase.cs @@ -25,7 +25,7 @@ public ITestRenderer Renderer } /// - public virtual TestServiceProvider Services { get; } + public TestServiceProvider Services { get; } /// /// Creates a new instance of the class. diff --git a/src/bunit.core/TestServiceProvider.cs b/src/bunit.core/TestServiceProvider.cs index 10be83f6a..548574459 100644 --- a/src/bunit.core/TestServiceProvider.cs +++ b/src/bunit.core/TestServiceProvider.cs @@ -23,7 +23,7 @@ public sealed class TestServiceProvider : IServiceProvider, IServiceCollection, /// Gets whether this has been initialized, and /// no longer will accept calls to the AddService's methods. /// - public bool IsProviderInitialized => _serviceProvider is { }; + public bool IsProviderInitialized => _serviceProvider is not null; /// public int Count => _serviceCollection.Count; diff --git a/src/bunit.web/Asserting/ShouldBeAdditionAssertExtensions.cs b/src/bunit.web/Asserting/ShouldBeAdditionAssertExtensions.cs index 015149283..9e0653564 100644 --- a/src/bunit.web/Asserting/ShouldBeAdditionAssertExtensions.cs +++ b/src/bunit.web/Asserting/ShouldBeAdditionAssertExtensions.cs @@ -29,7 +29,7 @@ public static void ShouldBeAddition(this IDiff actualChange, string expectedChan var actual = actualChange as UnexpectedNodeDiff ?? throw new DiffChangeAssertException(actualChange.Result, DiffResult.Unexpected, "The change was not an addition."); INodeList expected; - if (actual.Test.Node.GetHtmlParser() is { } parser) + if (actual.Test.Node.GetHtmlParser() is HtmlParser parser) { expected = parser.Parse(expectedChange); } diff --git a/src/bunit.web/Asserting/ShouldBeRemovalAssertExtensions.cs b/src/bunit.web/Asserting/ShouldBeRemovalAssertExtensions.cs index 9eea10a77..842a11456 100644 --- a/src/bunit.web/Asserting/ShouldBeRemovalAssertExtensions.cs +++ b/src/bunit.web/Asserting/ShouldBeRemovalAssertExtensions.cs @@ -29,7 +29,7 @@ public static void ShouldBeRemoval(this IDiff actualChange, string expectedChang var actual = actualChange as MissingNodeDiff ?? throw new DiffChangeAssertException(actualChange.Result, DiffResult.Missing, "The change was not an removal."); INodeList expected; - if (actual.Control.Node.GetHtmlParser() is { } parser) + if (actual.Control.Node.GetHtmlParser() is HtmlParser parser) { expected = parser.Parse(expectedChange); } diff --git a/src/bunit.web/Extensions/TestRendererExtensions.cs b/src/bunit.web/Extensions/TestRendererExtensions.cs index 4d141a7f5..76253192d 100644 --- a/src/bunit.web/Extensions/TestRendererExtensions.cs +++ b/src/bunit.web/Extensions/TestRendererExtensions.cs @@ -22,7 +22,7 @@ public static IRenderedComponent RenderComponent(this IT { if (renderer is null) throw new ArgumentNullException(nameof(renderer)); - var resultBase = renderer.RenderComponent(parameters); + var resultBase = renderer.RenderComponent(new ComponentParameterCollection { parameters }); if (resultBase is IRenderedComponent result) return result; else @@ -36,15 +36,13 @@ public static IRenderedComponent RenderComponent(this IT /// The renderer to use. /// The a builder to create parameters to pass to the component. /// A that provides access to the rendered component. - public static IRenderedComponent RenderComponent(this ITestRenderer renderer, Action> parameterBuilder) + public static IRenderedComponent RenderComponent(this ITestRenderer renderer, Action> parameterBuilder) where TComponent : IComponent { if (renderer is null) throw new ArgumentNullException(nameof(renderer)); if (parameterBuilder is null) throw new ArgumentNullException(nameof(parameterBuilder)); - var builder = new ComponentParameterBuilder(); - parameterBuilder(builder); - + var builder = new ComponentParameterCollectionBuilder(parameterBuilder); var resultBase = renderer.RenderComponent(builder.Build()); if (resultBase is IRenderedComponent result) return result; diff --git a/src/bunit.web/RazorTesting/Fixture.cs b/src/bunit.web/RazorTesting/Fixture.cs index e484ab4dd..bb35dd0f7 100644 --- a/src/bunit.web/RazorTesting/Fixture.cs +++ b/src/bunit.web/RazorTesting/Fixture.cs @@ -132,7 +132,7 @@ private IRenderedFragment Factory(RenderFragment fragment) return (IRenderedFragment)Renderer.RenderFragment(fragment); } - private IRenderedComponent TryCastTo(IRenderedFragment target, [System.Runtime.CompilerServices.CallerMemberName] string sourceMethod = "") where TComponent : IComponent + private static IRenderedComponent TryCastTo(IRenderedFragment target, [System.Runtime.CompilerServices.CallerMemberName] string sourceMethod = "") where TComponent : IComponent { if (target is IRenderedComponent result) { @@ -145,15 +145,8 @@ private IRenderedComponent TryCastTo(IRenderedFragment t $"That cannot be cast to an object of type IRenderedComponent<{typeof(TComponent).Name}>."); } - if (target is IRenderedFragmentBase) - { - throw new InvalidOperationException($"It is not possible to call the generic version of {sourceMethod} after " + - $"the non-generic version has been called on the same test context. Change all calls to the same generic version and try again."); - } - else - { - throw new Exception($"This line should never have been reached. An unknown type was placed inside the {nameof(_renderedFragments)}."); - } + throw new InvalidOperationException($"It is not possible to call the generic version of {sourceMethod} after " + + $"the non-generic version has been called on the same test context. Change all calls to the same generic version and try again."); } /// diff --git a/src/bunit.web/RazorTesting/SnapshotTest.cs b/src/bunit.web/RazorTesting/SnapshotTest.cs index 0fbca41dd..2af48661e 100644 --- a/src/bunit.web/RazorTesting/SnapshotTest.cs +++ b/src/bunit.web/RazorTesting/SnapshotTest.cs @@ -46,9 +46,9 @@ protected override async Task Run() Services.AddDefaultTestContextServices(); - if (Setup is { }) + if (Setup is not null) TryRun(Setup, this); - if (SetupAsync is { }) + if (SetupAsync is not null) await TryRunAsync(SetupAsync, this).ConfigureAwait(false); var renderedTestInput = (IRenderedFragment)Renderer.RenderFragment(TestInput!); diff --git a/src/bunit.web/TestContext.cs b/src/bunit.web/TestContext.cs index 75fab7eba..9b0f73c33 100644 --- a/src/bunit.web/TestContext.cs +++ b/src/bunit.web/TestContext.cs @@ -33,7 +33,7 @@ public IRenderedComponent RenderComponent(params Compone /// Type of the component to render /// The ComponentParameterBuilder action to add type safe parameters to pass to the component when it is rendered /// The rendered - public virtual IRenderedComponent RenderComponent(Action> parameterBuilder) where TComponent : IComponent + public virtual IRenderedComponent RenderComponent(Action> parameterBuilder) where TComponent : IComponent => TestRendererExtensions.RenderComponent(Renderer, parameterBuilder); } } diff --git a/src/bunit.web/TestDoubles/Authorization/FakeAuthenticationStateProvider.cs b/src/bunit.web/TestDoubles/Authorization/FakeAuthenticationStateProvider.cs index d2abd3d7a..0e7594b48 100644 --- a/src/bunit.web/TestDoubles/Authorization/FakeAuthenticationStateProvider.cs +++ b/src/bunit.web/TestDoubles/Authorization/FakeAuthenticationStateProvider.cs @@ -103,7 +103,7 @@ private static AuthenticationState CreateAuthenticationState( var testPrincipal = new FakePrincipal { Identity = identity, Roles = roles ?? Array.Empty() }; var principal = new ClaimsPrincipal(testPrincipal); - if (claims is { } && claims.Any()) + if (claims is not null && claims.Any()) { principal.AddIdentity(new ClaimsIdentity(claims)); } diff --git a/src/bunit.web/TestDoubles/Authorization/FakeAuthorizationService.cs b/src/bunit.web/TestDoubles/Authorization/FakeAuthorizationService.cs index ed3b96a3d..96a721d81 100644 --- a/src/bunit.web/TestDoubles/Authorization/FakeAuthorizationService.cs +++ b/src/bunit.web/TestDoubles/Authorization/FakeAuthorizationService.cs @@ -78,7 +78,7 @@ public Task AuthorizeAsync(ClaimsPrincipal user, object? re { result = VerifyRequiredRoles(requirements); } - else if (_supportedPolicies is { }) + else if (_supportedPolicies is not null) { result = VerifyRequiredPolicies(requirements); } diff --git a/src/bunit.web/TestDoubles/Authorization/MissingFakeAuthorizationException.cs b/src/bunit.web/TestDoubles/Authorization/MissingFakeAuthorizationException.cs index d45d8f5d0..fa30dadec 100644 --- a/src/bunit.web/TestDoubles/Authorization/MissingFakeAuthorizationException.cs +++ b/src/bunit.web/TestDoubles/Authorization/MissingFakeAuthorizationException.cs @@ -6,7 +6,7 @@ namespace Bunit.TestDoubles.Authorization /// Exception used to indicate that the fake authorization services are required by a test /// but provided in TestContext.Services. /// - public class MissingFakeAuthorizationException : Exception + public sealed class MissingFakeAuthorizationException : Exception { /// /// Creates a new instance of the @@ -17,7 +17,7 @@ public MissingFakeAuthorizationException(string serviceName) : base($"This test requires {serviceName} to be supplied, because the component under test uses authentication/authorization during the test. You can fix this by calling TestContext.Services.AddAuthorization with appropriate values. More information can be found in the documentation.") { ServiceName = serviceName; - HelpLink = "https://bunit.egilhansen.com/docs/test-doubles/faking-auth.html"; + HelpLink = "https://bunit.egilhansen.com/docs/test-doubles/faking-auth"; } /// diff --git a/src/bunit.web/TestDoubles/JSInterop/MockJSRuntimeInvokeHandler.cs b/src/bunit.web/TestDoubles/JSInterop/MockJSRuntimeInvokeHandler.cs index 42e4b4810..6427447a3 100644 --- a/src/bunit.web/TestDoubles/JSInterop/MockJSRuntimeInvokeHandler.cs +++ b/src/bunit.web/TestDoubles/JSInterop/MockJSRuntimeInvokeHandler.cs @@ -149,7 +149,7 @@ public ValueTask InvokeAsync(string identifier, CancellationToke var planned = plannedInvocations.OfType>() .SingleOrDefault(x => x.Matches(invocation)); - if (planned is { }) + if (planned is not null) { var task = planned.RegisterInvocation(invocation); result = new ValueTask(task); diff --git a/src/bunit.xunit/Xunit.Sdk/RazorTestRunner.cs b/src/bunit.xunit/Xunit.Sdk/RazorTestRunner.cs index a22fbc59b..73b6e822c 100644 --- a/src/bunit.xunit/Xunit.Sdk/RazorTestRunner.cs +++ b/src/bunit.xunit/Xunit.Sdk/RazorTestRunner.cs @@ -34,7 +34,7 @@ private string GetTestOutput() { string result = string.Empty; - if (_testOutputHelper is { }) + if (_testOutputHelper is not null) { result = _testOutputHelper.Output; _testOutputHelper.Uninitialize(); diff --git a/src/bunit.xunit/Xunit.Sdk/RazorTestSourceInformationProvider.cs b/src/bunit.xunit/Xunit.Sdk/RazorTestSourceInformationProvider.cs index 858d7e24f..f5d8b48c9 100644 --- a/src/bunit.xunit/Xunit.Sdk/RazorTestSourceInformationProvider.cs +++ b/src/bunit.xunit/Xunit.Sdk/RazorTestSourceInformationProvider.cs @@ -85,7 +85,7 @@ private bool TryFindSourceFile(Type testComponent, [NotNullWhen(true)] out strin break; } - return razorFile is { }; + return razorFile is not null; } private SourceFileFinder GetSourceFileFinderForType(Type testComponent) @@ -128,7 +128,7 @@ private static bool TryGetRazorFileFromGeneratedFile(string file, [NotNullWhen(t result = line[GENERATED_FILE_REF_PREFIX.Length..refFileEndIndex]; } - return result is { }; + return result is not null; } private static int? FindLineNumber(string razorFile, RazorTestBase test, int testNumber) diff --git a/tests/.editorconfig b/tests/.editorconfig new file mode 100644 index 000000000..96667de25 --- /dev/null +++ b/tests/.editorconfig @@ -0,0 +1,5 @@ +root = false + +[*.cs] +# IDE0039: Use local function +csharp_style_pattern_local_over_anonymous_function = false:suggestion diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 6b42e4c67..60f780816 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -2,9 +2,11 @@ netcoreapp3.1;net5.0 false - 8.0 + 9.0 enable CS8600;CS8602;CS8603;CS8625 + true + true diff --git a/tests/bunit.core.tests/ComponentParameterBuilderTests.cs b/tests/bunit.core.tests/ComponentParameterBuilderTests.cs deleted file mode 100644 index 2cb76cfe5..000000000 --- a/tests/bunit.core.tests/ComponentParameterBuilderTests.cs +++ /dev/null @@ -1,391 +0,0 @@ -using System; -using System.Threading.Tasks; -using Bunit.TestAssets.SampleComponents; -using Microsoft.AspNetCore.Components; -using Shouldly; -using Xunit; - -namespace Bunit -{ - public class ComponentParameterBuilderTests - { - [Fact(DisplayName = "Add with a parameterSelector for a CascadingParameter and a nullable integer as value and Build should return the correct ComponentParameters")] - public void Test001() - { - // Arrange - var sut = CreateSut(); - const int value = 42; - - // Arrange - sut.Add(c => c.NamedCascadingValue, value); - var result = sut.Build(); - - // Assert - result.Count.ShouldBe(1); - - var parameter = result[0]; - parameter.IsCascadingValue.ShouldBeTrue(); - parameter.Name.ShouldBe(nameof(AllTypesOfParams.NamedCascadingValue)); - parameter.Value.ShouldBe(value); - } - - [Theory(DisplayName = "Add with a parameterSelector for a Parameter and a string as value and Build should return the correct ComponentParameters")] - [InlineData(null)] - [InlineData("")] - [InlineData("foo")] - public void Test002(string? value) - { - // Arrange - var sut = CreateSut(); - - // Act - sut.Add(c => c.RegularParam, value); - var result = sut.Build(); - - // Assert - result.Count.ShouldBe(1); - - var parameter = result[0]; - parameter.IsCascadingValue.ShouldBeFalse(); - parameter.Name.ShouldBe(nameof(AllTypesOfParams.RegularParam)); - parameter.Value.ShouldBe(value); - } - - [Fact(DisplayName = "Add with a parameterSelector for a RenderFragment and a markup string as value and Build should return the correct ComponentParameters")] - public void Test003() - { - // Arrange - var sut = CreateSut(); - string value = "test"; - - // Act - sut.Add(c => c.OtherContent, value); - var result = sut.Build(); - - // Assert - result.Count.ShouldBe(1); - - var parameter = result[0]; - parameter.IsCascadingValue.ShouldBeFalse(); - parameter.Name.ShouldBe(nameof(AllTypesOfParams.OtherContent)); - parameter.Value.ShouldBeOfType(); - } - - [Fact(DisplayName = "Add with a parameterSelector for a RenderFragment and a markupFactory as value and Build should return the correct ComponentParameters")] - public void Test004() - { - // Arrange - var sut = CreateSut(); - - // Act - sut.Add(c => c.ItemTemplate, num => $"

{num}

"); - var result = sut.Build(); - - // Assert - result.Count.ShouldBe(1); - - var parameter = result[0]; - parameter.IsCascadingValue.ShouldBeFalse(); - parameter.Name.ShouldBe(nameof(AllTypesOfParams.ItemTemplate)); - parameter.Value.ShouldBeOfType>(); - } - - [Fact(DisplayName = "Add with a parameterSelector for a template (RenderFragment) and a template as value and Build should return the correct ComponentParameters")] - public void Test005() - { - // Arrange - var sut = CreateSut(); - - // Act - sut.Add(c => c.ItemTemplate, num => builder => builder.AddMarkupContent(0, $"

{num}

")); - var result = sut.Build(); - - // Assert - result.Count.ShouldBe(1); - - var parameter = result[0]; - parameter.IsCascadingValue.ShouldBeFalse(); - parameter.Name.ShouldBe(nameof(AllTypesOfParams.ItemTemplate)); - parameter.Value.ShouldBeOfType>(); - } - - [Fact(DisplayName = "Add with a parameterSelector for a NonGenericEventCallback and a async-callback as value and Build should return the correct ComponentParameters")] - public void Test006() - { - // Arrange - var sut = CreateSut(); - var @event = EventCallback.Empty; - Func callback = () => Task.FromResult(@event); - - // Act - sut.Add(c => c.NonGenericCallback, callback); - var result = sut.Build(); - - // Assert - result.Count.ShouldBe(1); - - var parameter = result[0]; - parameter.IsCascadingValue.ShouldBeFalse(); - parameter.Name.ShouldBe(nameof(AllTypesOfParams.NonGenericCallback)); - parameter.Value.ShouldNotBeNull(); - } - - [Fact(DisplayName = "Add with a parameterSelector for a NonGenericEventCallback and a callback as value and Build should return the correct ComponentParameters")] - public void Test007() - { - // Arrange - var sut = CreateSut(); - - // Act - sut.Add(c => c.NonGenericCallback, () => throw new Exception("NonGenericCallback")); - var result = sut.Build(); - - // Assert - result.Count.ShouldBe(1); - - var parameter = result[0]; - parameter.IsCascadingValue.ShouldBeFalse(); - parameter.Name.ShouldBe(nameof(AllTypesOfParams.NonGenericCallback)); - parameter.Value.ShouldNotBeNull(); - } - - [Fact(DisplayName = "Add with a parameterSelector for a GenericEventCallback and a async-callback as value and Build should return the correct ComponentParameters")] - public void Test008() - { - // Arrange - var sut = CreateSut(); - var @event = EventCallback.Empty; - Func callback = (args) => Task.FromResult(@event); - - // Act - sut.Add(c => c.GenericCallback, callback); - var result = sut.Build(); - - // Assert - result.Count.ShouldBe(1); - - var parameter = result[0]; - parameter.IsCascadingValue.ShouldBeFalse(); - parameter.Name.ShouldBe(nameof(AllTypesOfParams.GenericCallback)); - parameter.Value.ShouldNotBeNull(); - } - - [Fact(DisplayName = "Add with a parameterSelector for a GenericEventCallback and a callback as value and Build should return the correct ComponentParameters")] - public void Test009() - { - // Arrange - var sut = CreateSut(); - - // Act - sut.Add(c => c.GenericCallback, args => throw new Exception("GenericCallback")); - var result = sut.Build(); - - // Assert - result.Count.ShouldBe(1); - - var parameter = result[0]; - parameter.IsCascadingValue.ShouldBeFalse(); - parameter.Name.ShouldBe(nameof(AllTypesOfParams.GenericCallback)); - parameter.Value.ShouldNotBeNull(); - } - - [Fact(DisplayName = "Add with multiple mixed parameterSelectors and valid values and Build should return the correct ComponentParameters")] - public void Test010() - { - // Arrange - var sut = CreateSut(); - - // Act - sut.Add(c => c.NamedCascadingValue, 42).Add(c => c.RegularParam, "bar"); - var result = sut.Build(); - - // Assert - result.Count.ShouldBe(2); - - var first = result[0]; - first.IsCascadingValue.ShouldBeTrue(); - first.Name.ShouldBe(nameof(AllTypesOfParams.NamedCascadingValue)); - first.Value.ShouldBe(42); - - var second = result[1]; - second.IsCascadingValue.ShouldBeFalse(); - second.Name.ShouldBe(nameof(AllTypesOfParams.RegularParam)); - second.Value.ShouldBe("bar"); - } - - [Fact(DisplayName = "Add with a parameterSelectors for multiple RenderFragments and childBuilders as values and Build should return the correct ComponentParameters")] - public void Test011() - { - // Arrange - var sut = CreateSut(); - - // Act - sut.Add(wrapper => wrapper.First, childBuilder => - { - childBuilder - .Add(c => c.Header, "H1") - .Add(c => c.AttrValue, "A1"); - }) - .Add>(wrapper => wrapper.Second, childBuilder => - { - childBuilder - .Add(c => c.RegularParam, "test"); - }); - var result = sut.Build(); - - // Assert - result.Count.ShouldBe(2); - - var first = result[0]; - first.IsCascadingValue.ShouldBeFalse(); - first.Name.ShouldBe(nameof(TwoComponentWrapper.First)); - first.Value.ShouldBeOfType(); - - var second = result[1]; - second.IsCascadingValue.ShouldBeFalse(); - second.Name.ShouldBe(nameof(TwoComponentWrapper.Second)); - second.Value.ShouldBeOfType(); - } - - [Fact(DisplayName = "AddChildContent with a childBuilders and Build should return the correct ComponentParameters")] - public void Test012() - { - // Arrange - var sut = CreateSut(); - - // Act - sut.AddChildContent(childBuilder => - { - childBuilder - .Add(c => c.Header, "H1") - .Add(c => c.AttrValue, "A1"); - }); - var result = sut.Build(); - - // Assert - result.Count.ShouldBe(1); - - var first = result[0]; - first.IsCascadingValue.ShouldBeFalse(); - first.Name.ShouldBe(nameof(Wrapper.ChildContent)); - first.Value.ShouldBeOfType(); - } - - [Fact(DisplayName = "AddChildContent with a string markup and Build should return the correct ComponentParameters")] - public void Test013() - { - // Arrange - var sut = CreateSut(); - - // Act - sut.AddChildContent("x"); - var result = sut.Build(); - - // Assert - result.Count.ShouldBe(1); - - var first = result[0]; - first.IsCascadingValue.ShouldBeFalse(); - first.Name.ShouldBe(nameof(Wrapper.ChildContent)); - first.Value.ShouldBeOfType(); - } - - [Fact(DisplayName = "Add unnamed CascadingParameter with a value and Build should return the correct ComponentParameters")] - public void Test014() - { - // Arrange - var sut = CreateSut(); - const int value = 42; - - // Act - sut.Add(value); - var result = sut.Build(); - - // Assert - result.Count.ShouldBe(1); - - var parameter = result[0]; - parameter.IsCascadingValue.ShouldBeTrue(); - parameter.Name.ShouldBeNull(); - parameter.Value.ShouldBe(value); - } - - [Fact(DisplayName = "AddUnmatched with a key and value and Build should return the correct ComponentParameters")] - public void Test015() - { - // Arrange - var sut = CreateSut(); - const string key = "some-unmatched-attribute"; - const int value = 42; - - // Arrange - sut.AddUnmatched(key, value); - var result = sut.Build(); - - // Assert - result.Count.ShouldBe(1); - - var parameter = result[0]; - parameter.IsCascadingValue.ShouldBeFalse(); - parameter.Name.ShouldBe(key); - parameter.Value.ShouldBe(value); - } - - [Fact(DisplayName = "Add duplicate name should throw Exception")] - public void Test100() - { - // Arrange - var sut = CreateSut(); - sut.Add(c => c.NamedCascadingValue, 42); - - // Act and Assert - Assert.Throws(() => sut.Add(c => c.NamedCascadingValue, 43)); - } - - [Fact(DisplayName = "Add CascadingParameter (with null value) should throw Exception")] - public void Test101() - { - // Arrange - var sut = CreateSut(); - - // Act and Assert - Assert.Throws(() => sut.Add(c => c.NamedCascadingValue, null)); - } - - [Fact(DisplayName = "Add with a property which does not have the [Parameter] or [CascadingParameter] attribute defined should throw Exception")] - public void Test102() - { - // Arrange - var sut = CreateSut(); - - // Act and Assert - Assert.Throws(() => sut.Add(c => c.NoParameterProperty, 42)); - } - - [Fact(DisplayName = "AddChildContent without a ChildContent property defined should throw Exception")] - public void Test103() - { - // Arrange - var sut = CreateSut(); - - // Act and Assert - Assert.Throws(() => sut.AddChildContent("html")); - } - - [Fact(DisplayName = "Add with a selectorExpression which is not a property should throw Exception")] - public void Test104() - { - // Arrange - var sut = CreateSut(); - - // Act and Assert - Assert.Throws(() => sut.Add(c => c.DummyMethod(), 42)); - } - - private static ComponentParameterBuilder> CreateSut() - => CreateSut>(); - - private static ComponentParameterBuilder CreateSut() where TComponent : IComponent - => new ComponentParameterBuilder(); - } -} diff --git a/tests/bunit.core.tests/ComponentParameterCollectionBuilderTests.cs b/tests/bunit.core.tests/ComponentParameterCollectionBuilderTests.cs new file mode 100644 index 000000000..6dbf1a28a --- /dev/null +++ b/tests/bunit.core.tests/ComponentParameterCollectionBuilderTests.cs @@ -0,0 +1,528 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using Bunit.Rendering; +using Microsoft.AspNetCore.Components; +using Shouldly; +using Xunit; + +namespace Bunit +{ + public class ComponentParameterCollectionBuilderTests + { + private readonly ComponentParameterCollectionBuilder Builder = new ComponentParameterCollectionBuilder(); + + private bool EventCallbackCalled { get; set; } + + private void VerifyParameter(string expectedName, [AllowNull] T expectedInput) + { + Builder.Build() + .ShouldHaveSingleItem() + .ShouldBeParameter( + name: expectedName, + value: expectedInput, + isCascadingValue: false + ); + } + + private async Task VerifyEventCallback(string expectedName) + { + var actual = Builder.Build() + .ShouldHaveSingleItem() + .ShouldBeParameter(name: expectedName, isCascadingValue: false); + await actual.InvokeAsync(EventArgs.Empty); + EventCallbackCalled.ShouldBeTrue(); + } + + private async Task VerifyEventCallback(string expectedName) where T : new() + { + var actual = Builder.Build() + .ShouldHaveSingleItem() + .ShouldBeParameter>(name: expectedName, isCascadingValue: false); + await actual.InvokeAsync(new T()); + EventCallbackCalled.ShouldBeTrue(); + } + + private static IRenderedFragment RenderWithRenderFragment(RenderFragment renderFragment) + { + var ctx = new TestContext(); + return (IRenderedFragment)ctx.Renderer.RenderFragment(renderFragment); + } + + private static IRenderedComponent RenderWithRenderFragment(RenderFragment renderFragment) where TComponent : IComponent + { + var ctx = new TestContext(); + var res = (IRenderedFragment)ctx.Renderer.RenderFragment(renderFragment); + return res.FindComponent(); + } + + [Fact(DisplayName = "Null for parameter selector throws")] + public void Test000() + { + Should.Throw(() => Builder.Add(default!, 42)); + } + + [Fact(DisplayName = "Selecting a non property with parameter selector throws")] + public void Test0000() + { + Should.Throw(() => Builder.Add(x => x._nonParam, 42)); + } + + [Fact(DisplayName = "Selecting a non parameter property with parameter selector throws")] + public void Test00000() + { + Should.Throw(() => Builder.Add(x => x.NonParamProp, new object())); + } + + [Fact(DisplayName = "Value type with parameter selector")] + public void Test001() + { + Builder.Add(x => x.ValueTypeParam, 42); + VerifyParameter("ValueTypeParam", 42); + } + + [Fact(DisplayName = "Null for struct? with parameter selector")] + public void Test002() + { + Builder.Add(x => x.NullableValueTypeParam, null); + VerifyParameter("NullableValueTypeParam", null); + } + + [Fact(DisplayName = "Struct? with parameter selector")] + public void Test003() + { + Builder.Add(x => x.NullableValueTypeParam, 1234); + VerifyParameter("NullableValueTypeParam", 1234); + } + + [Fact(DisplayName = "Object with parameter selector")] + public void Test004() + { + var input = new object(); + Builder.Add(x => x.Param, input); + VerifyParameter("Param", input); + } + + [Fact(DisplayName = "Null for object with parameter selector")] + public void Test005() + { + Builder.Add(x => x.Param, null); + VerifyParameter("Param", null); + } + + [Fact(DisplayName = "EventCallback with parameter selector")] + public void Test010() + { + var input = EventCallback.Empty; + Builder.Add(x => x.EC, input); + VerifyParameter("EC", input); + } + + [Fact(DisplayName = "Null to EventCallback throws")] + public void Test011() + { + Should.Throw(() => Builder.Add(x => x.EC, null!)); + } + + [Fact(DisplayName = "Null for EventCallback? with parameter selector")] + public void Test011_2() + { + Builder.Add(x => x.NullableEC, null); + VerifyParameter("NullableEC", null); + } + + [Fact(DisplayName = "Action to EventCallback with parameter selector")] + public async Task Test012() + { + Builder.Add(x => x.EC, () => { EventCallbackCalled = true; }); + await VerifyEventCallback("EC"); + } + + [Fact(DisplayName = "Action to EventCallback with parameter selector")] + public async Task Test013() + { + Builder.Add(x => x.EC, (x) => { EventCallbackCalled = true; }); + await VerifyEventCallback("EC"); + } + + [Fact(DisplayName = "Func to EventCallback with parameter selector")] + public async Task Test014() + { + Builder.Add(x => x.EC, () => { EventCallbackCalled = true; return Task.CompletedTask; }); + await VerifyEventCallback("EC"); + } + + [Fact(DisplayName = "Action to EventCallback? with parameter selector")] + public async Task Test015() + { + Builder.Add(x => x.NullableEC, () => { EventCallbackCalled = true; }); + await VerifyEventCallback("NullableEC"); + } + + [Fact(DisplayName = "Action to EventCallback? with parameter selector")] + public async Task Test016() + { + Builder.Add(x => x.NullableEC, (x) => { EventCallbackCalled = true; }); + await VerifyEventCallback("NullableEC"); + } + + [Fact(DisplayName = "Func to EventCallback? with parameter selector")] + public async Task Test017() + { + Builder.Add(x => x.NullableEC, () => { EventCallbackCalled = true; return Task.CompletedTask; }); + await VerifyEventCallback("NullableEC"); + } + + [Fact(DisplayName = "EventCallback with parameter selector")] + public void Test018() + { + var input = EventCallback.Empty; + Builder.Add(x => x.ECWithArgs, input); + VerifyParameter("ECWithArgs", input); + } + + [Fact(DisplayName = "Action to EventCallback with parameter selector")] + public async Task Test019() + { + Builder.Add(x => x.ECWithArgs, () => { EventCallbackCalled = true; }); + await VerifyEventCallback("ECWithArgs"); + } + + [Fact(DisplayName = "Action to EventCallback with parameter selector")] + public async Task Test020() + { + Builder.Add(x => x.ECWithArgs, (x) => { EventCallbackCalled = true; }); + await VerifyEventCallback("ECWithArgs"); + } + + [Fact(DisplayName = "Func to EventCallback with parameter selector")] + public async Task Test021() + { + Builder.Add(x => x.ECWithArgs, () => { EventCallbackCalled = true; return Task.CompletedTask; }); + await VerifyEventCallback("ECWithArgs"); + } + + [Fact(DisplayName = "EventCallback with parameter selector")] + public void Test022() + { + var input = EventCallback.Empty; + Builder.Add(x => x.NullableECWithArgs, input); + VerifyParameter("NullableECWithArgs", input); + } + + [Fact(DisplayName = "Action to EventCallback with parameter selector")] + public async Task Test023() + { + Builder.Add(x => x.NullableECWithArgs, () => { EventCallbackCalled = true; }); + await VerifyEventCallback("NullableECWithArgs"); + } + + [Fact(DisplayName = "Action to EventCallback with parameter selector")] + public async Task Test024() + { + Builder.Add(x => x.NullableECWithArgs, (x) => { EventCallbackCalled = true; }); + await VerifyEventCallback("NullableECWithArgs"); + } + + [Fact(DisplayName = "Func to EventCallback with parameter selector")] + public async Task Test025() + { + Builder.Add(x => x.NullableECWithArgs, () => { EventCallbackCalled = true; return Task.CompletedTask; }); + await VerifyEventCallback("NullableECWithArgs"); + } + + [Fact(DisplayName = "ChildContent can be passed as RenderFragment")] + public void Test030() + { + RenderFragment input = b => b.AddMarkupContent(0, ""); + Builder.AddChildContent(input); + VerifyParameter("ChildContent", input); + } + + [Fact(DisplayName = "Calling AddChildContent when TCompnent does not have a parameter named ChildContent throws")] + public void Test031() + { + RenderFragment input = b => b.AddMarkupContent(0, ""); + Assert.Throws(() => new ComponentParameterCollectionBuilder().AddChildContent(input)); + Assert.Throws(() => new ComponentParameterCollectionBuilder().AddChildContent(input)); + } + + [Fact(DisplayName = "ChildContent can be passed as a nested component parameter builder")] + public void Test032() + { + Builder.AddChildContent(parameters => parameters.Add(p => p.ValueTypeParam, 42)); + + var actual = Builder.Build().ShouldHaveSingleItem() + .ShouldBeParameter("ChildContent", isCascadingValue: false); + var actualComponent = RenderWithRenderFragment(actual); + actualComponent.Instance.ValueTypeParam.ShouldBe(42); + } + + [Fact(DisplayName = "ChildContent can be passed as a child component without parameters")] + public void Test033() + { + Builder.AddChildContent(); + + var actual = Builder.Build().ShouldHaveSingleItem() + .ShouldBeParameter("ChildContent", isCascadingValue: false); + var actualComponent = RenderWithRenderFragment(actual); + actualComponent.Instance.ShouldBeOfType(); + } + + [Fact(DisplayName = "ChildContent can be passed as a markup string")] + public void Test034() + { + var input = "

42

"; + Builder.AddChildContent(input); + + var actual = Builder.Build().ShouldHaveSingleItem() + .ShouldBeParameter("ChildContent", isCascadingValue: false); + var actualComponent = RenderWithRenderFragment(actual); + actualComponent.Markup.ShouldBe(input); + } + + [Fact(DisplayName = "RenderFragment can be passed as a nested component parameter builder")] + public void Test040() + { + Builder.Add(x => x.OtherFragment, parameters => parameters.Add(p => p.ValueTypeParam, 42)); + + var actual = Builder.Build().ShouldHaveSingleItem() + .ShouldBeParameter("OtherFragment", isCascadingValue: false); + var actualComponent = RenderWithRenderFragment(actual); + actualComponent.Instance.ValueTypeParam.ShouldBe(42); + } + + [Fact(DisplayName = "RenderFragment can be passed as a child component without parameters")] + public void Test041() + { + Builder.Add(x => x.OtherFragment); + + var actual = Builder.Build().ShouldHaveSingleItem() + .ShouldBeParameter("OtherFragment", isCascadingValue: false); + var actualComponent = RenderWithRenderFragment(actual); + actualComponent.Instance.ShouldBeOfType(); + } + + [Fact(DisplayName = "RenderFragment can be passed as a markup string")] + public void Test042() + { + var input = "

42

"; + Builder.Add(x => x.OtherFragment, input); + + var actual = Builder.Build().ShouldHaveSingleItem() + .ShouldBeParameter("OtherFragment", isCascadingValue: false); + var actualComponent = RenderWithRenderFragment(actual); + actualComponent.Markup.ShouldBe(input); + } + + [Fact(DisplayName = "RenderFragment can be passed RenderFragment")] + public void Test043() + { + RenderFragment input = b => b.AddMarkupContent(0, ""); + + Builder.Add(x => x.OtherFragment, input); + + Builder.Build().ShouldHaveSingleItem() + .ShouldBeParameter("OtherFragment", input, isCascadingValue: false); + } + + [Fact(DisplayName = "RenderFragment can be passed multiple times")] + public void Test044() + { + var input = "FOO"; + Builder.Add(x => x.OtherFragment, input); + Builder.Add(x => x.OtherFragment, b => b.AddMarkupContent(0, input)); + + Builder.Build().ShouldAllBe(VerifyTemplate, VerifyTemplate); + + void VerifyTemplate(ComponentParameter template) + { + var actual = template.ShouldBeParameter("OtherFragment", isCascadingValue: false); + var actualComponent = RenderWithRenderFragment(actual); + actualComponent.Markup.ShouldBe(input); + } + } + + [Fact(DisplayName = "RenderFragment? can be passed as RenderFragment")] + public void Test050() + { + RenderFragment input = s => b => b.AddMarkupContent(0, s); + + Builder.Add(x => x.Template, input); + + Builder.Build().ShouldHaveSingleItem() + .ShouldBeParameter("Template", input, isCascadingValue: false); + } + + [Fact(DisplayName = "RenderFragment? can be passed lambda builder")] + public void Test051() + { + var input = "FOO"; + Builder.Add(x => x.Template, value => value); + + var actual = Builder.Build().ShouldHaveSingleItem() + .ShouldBeParameter>("Template", isCascadingValue: false); + var actualComponent = RenderWithRenderFragment(actual(input)); + actualComponent.Markup.ShouldBe(input); + } + + [Fact(DisplayName = "RenderFragment? can be passed as nested object builder")] + public void Test052() + { + var input = "FOO"; + Builder.Add( + x => x.Template, + value => parameters => parameters.Add(p => p.Param, value) + ); + + var actual = Builder.Build().ShouldHaveSingleItem() + .ShouldBeParameter>("Template", isCascadingValue: false); + var actualComponent = RenderWithRenderFragment(actual(input)); + actualComponent.Instance.Param.ShouldBe(input); + } + + [Fact(DisplayName = "RenderFragment can be passed multiple times")] + public void Test053() + { + Builder.Add(x => x.Template, value => value); + Builder.Add(x => x.Template, s => b => b.AddMarkupContent(0, s)); + + Builder.Build().ShouldAllBe(VerifyTemplate, VerifyTemplate); + + static void VerifyTemplate(ComponentParameter template) + { + var input = "FOO"; + var rf = template.ShouldBeParameter>("Template", isCascadingValue: false); + var actualComponent = RenderWithRenderFragment(rf(input)); + actualComponent.Markup.ShouldBe(input); + } + } + + [Fact(DisplayName = "Cascading values can be passed using Add and parameter selector")] + public void Test060() + { + Builder.Add(p => p.NullableCC, "FOO"); + Builder.Add(p => p.CC, 1); + Builder.Add(p => p.NullableNamedCC, "BAR"); + Builder.Add(p => p.NamedCC, 2); + Builder.Add(p => p.AnotherNamedCC, 3); + Builder.Add(p => p.RFCC, "BAZ"); + + Builder.Build().ShouldAllBe( + x => x.ShouldBeParameter(null, "FOO", isCascadingValue: true), + x => x.ShouldBeParameter(null, 1, isCascadingValue: true), + x => x.ShouldBeParameter("NullableNamedCCNAME", "BAR", isCascadingValue: true), + x => x.ShouldBeParameter("NamedCCNAME", 2, isCascadingValue: true), + x => x.ShouldBeParameter("AnotherNamedCCNAME", 3, isCascadingValue: true), + x => + { + var rf = x.ShouldBeParameter(null, isCascadingValue: true); + RenderWithRenderFragment(rf).Markup.ShouldBe("BAZ"); + } + ); + } + + [Fact(DisplayName = "AddCascadingValue can add unnamed cascading values")] + public void Test061() + { + var input = "FOO"; + + Builder.AddCascadingValue(input); + + Builder.Build().ShouldHaveSingleItem() + .ShouldBeParameter(null, input, isCascadingValue: true); + } + + [Fact(DisplayName = "AddCascadingValue can add named cascading values")] + public void Test062() + { + var name = "NAME"; + var input = "FOO"; + + Builder.AddCascadingValue(name, input); + + Builder.Build().ShouldHaveSingleItem() + .ShouldBeParameter(name, input, isCascadingValue: true); + } + + [Fact(DisplayName = "AddUnmatched can add unmatched empty value attributes as parameters")] + public void Test070() + { + var name = "NAME"; + + Builder.AddUnmatched(name); + + Builder.Build() + .ShouldHaveSingleItem() + .ShouldBeParameter(name, null, isCascadingValue: false); + } + + [Fact(DisplayName = "AddUnmatched can add unmatched value attributes as parameters")] + public void Test071() + { + var name = "NAME"; + var value = "FOO"; + + Builder.AddUnmatched(name, value); + + Builder.Build() + .ShouldHaveSingleItem() + .ShouldBeParameter(name, value, isCascadingValue: false); + } + + [Theory(DisplayName = "AddUnmatched throws if name is null or whitespace")] + [InlineData("")] + [InlineData(" ")] + public void Test072(string emptyName) + { + Should.Throw(() => Builder.AddUnmatched(emptyName)); + } + + [Fact(DisplayName = "AddUnmatched throws if component doesnt have an Parameter(CaptureUnmatchedValues = true)")] + public void Test073() + { + var sut = new ComponentParameterCollectionBuilder(); + + Should.Throw(() => sut.AddUnmatched("foo")); + } + + [Fact(DisplayName = "Can select parameters inherited from base component ")] + public void Test101() + { + var builder = new ComponentParameterCollectionBuilder(); + + builder.Add(x => x.Param, new object()); + + builder.Build().ShouldHaveSingleItem(); + } + + private class Params : ComponentBase + { + [Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary? Attributes { get; set; } + [Parameter] public int? NullableValueTypeParam { get; set; } + [Parameter] public int ValueTypeParam { get; set; } = -1; + [Parameter] public object? Param { get; set; } + [Parameter] public EventCallback? NullableEC { get; set; } + [Parameter] public EventCallback EC { get; set; } + [Parameter] public EventCallback? NullableECWithArgs { get; set; } + [Parameter] public EventCallback ECWithArgs { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public RenderFragment? OtherFragment { get; set; } + [Parameter] public RenderFragment? Template { get; set; } + [CascadingParameter] public string? NullableCC { get; set; } + [CascadingParameter] public int CC { get; set; } = -1; + [CascadingParameter(Name = nameof(NullableNamedCC) + "NAME")] public string? NullableNamedCC { get; set; } + [CascadingParameter(Name = nameof(NamedCC) + "NAME")] public int NamedCC { get; set; } = -1; + [CascadingParameter(Name = nameof(AnotherNamedCC) + "NAME")] public int AnotherNamedCC { get; set; } = -1; + [CascadingParameter] public RenderFragment? RFCC { get; set; } + public int _nonParam = -1; + public object? NonParamProp { get; set; } + public void SomeMethod() { } + } + + private class NoParams : ComponentBase { } + private class NonChildContentParameter : ComponentBase { public RenderFragment? ChildContent { get; set; } } + private class InhertedParams : Params { } + } +} diff --git a/tests/bunit.core.tests/ComponentParameterCollectionTest.cs b/tests/bunit.core.tests/ComponentParameterCollectionTest.cs new file mode 100644 index 000000000..b373ed904 --- /dev/null +++ b/tests/bunit.core.tests/ComponentParameterCollectionTest.cs @@ -0,0 +1,411 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Bunit.Extensions; +using Bunit.Rendering; +using Bunit.TestAssets.SampleComponents; +using Bunit.TestDoubles.JSInterop; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Xunit; + +namespace Bunit +{ + public class ComponentParameterCollectionTest + { + private static readonly TestContext Context = new TestContext(); + + static ComponentParameterCollectionTest() + { + Context.Services.AddMockJSRuntime(); + } + + private static IRenderedComponent RenderWithRenderFragment(RenderFragment renderFragment) + { + var res = (IRenderedFragment)Context.Renderer.RenderFragment(renderFragment); + return res.FindComponent(); + } + + [Fact(DisplayName = "ComponentParameters can be added to collection")] + public void Test001() + { + var sut = new ComponentParameterCollection(); + var p = ComponentParameter.CreateParameter("attr", 42); + + sut.Add(p); + + sut.Count.ShouldBe(1); + sut.Contains(p).ShouldBeTrue(); + } + + [Fact(DisplayName = "Add() throws if invalid parameter is passed to it")] + public void Test002() + { + var sut = new ComponentParameterCollection(); + var p = new ComponentParameter(); + + Should.Throw(() => sut.Add(p)); + } + + [Fact(DisplayName = "ToComponentRenderFragment creates RenderFragment for component when empty")] + public void Test010() + { + var sut = new ComponentParameterCollection(); + + var rf = sut.ToRenderFragment(); + + var c = RenderWithRenderFragment(rf).Instance; + c.VerifyParamsHaveDefaultValues(); + } + + [Fact(DisplayName = "ToComponentRenderFragment passes regular params to component in RenderFragment")] + public void Test011() + { + var sut = new ComponentParameterCollection + { + ComponentParameter.CreateParameter(nameof(Params.Param), "FOO"), + ComponentParameter.CreateParameter(nameof(Params.ValueTypeParam), 42), + ComponentParameter.CreateParameter(nameof(Params.NullableValueTypeParam), 1337) + }; + + var rf = sut.ToRenderFragment(); + + var c = RenderWithRenderFragment(rf).Instance; + c.Param.ShouldBe("FOO"); + c.ValueTypeParam.ShouldBe(42); + c.NullableValueTypeParam.ShouldBe(1337); + } + + [Fact(DisplayName = "ToComponentRenderFragment passes single ChildContent param to component in RenderFragment")] + public void Test012() + { + var sut = new ComponentParameterCollection + { + ComponentParameter.CreateParameter(nameof(Params.ChildContent), (RenderFragment)(b => b.AddMarkupContent(0, "FOO"))) + }; + + var rf = sut.ToRenderFragment(); + + var rc = RenderWithRenderFragment(rf); + rc.Markup.ShouldBe("FOO"); + } + + [Fact(DisplayName = "ToComponentRenderFragment passes multiple ChildContent params to component in RenderFragment")] + public void Test013() + { + var sut = new ComponentParameterCollection + { + ComponentParameter.CreateParameter(nameof(Params.ChildContent), (RenderFragment)(b => b.AddMarkupContent(0, "FOO"))), + ComponentParameter.CreateParameter(nameof(Params.ChildContent), (RenderFragment)(b => b.AddMarkupContent(0, "BAR"))), + ComponentParameter.CreateParameter(nameof(Params.ChildContent), (RenderFragment)(b => b.AddMarkupContent(0, "BAZ"))) + }; + + var rf = sut.ToRenderFragment(); + + var rc = RenderWithRenderFragment(rf); + rc.Markup.ShouldBe("FOOBARBAZ"); + } + + [Fact(DisplayName = "ToComponentRenderFragment passes single RenderFragment param to component in RenderFragment")] + public void Test014() + { + var sut = new ComponentParameterCollection + { + ComponentParameter.CreateParameter(nameof(Params.OtherFragment), (RenderFragment)(b => b.AddMarkupContent(0, "FOO"))) + }; + + var rf = sut.ToRenderFragment(); + + var rc = RenderWithRenderFragment(rf); + rc.Markup.ShouldBe("FOO"); + } + + [Fact(DisplayName = "ToComponentRenderFragment passes multiple RenderFragment params to component in RenderFragment")] + public void Test015() + { + var sut = new ComponentParameterCollection + { + ComponentParameter.CreateParameter(nameof(Params.OtherFragment), (RenderFragment)(b => b.AddMarkupContent(0, "FOO"))), + ComponentParameter.CreateParameter(nameof(Params.OtherFragment), (RenderFragment)(b => b.AddMarkupContent(0, "BAR"))), + ComponentParameter.CreateParameter(nameof(Params.OtherFragment), (RenderFragment)(b => b.AddMarkupContent(0, "BAZ"))) + }; + + var rf = sut.ToRenderFragment(); + + var rc = RenderWithRenderFragment(rf); + rc.Markup.ShouldBe("FOOBARBAZ"); + } + + [Fact(DisplayName = "ToComponentRenderFragment passes unmatched attributes to component in RenderFragment")] + public void Test016() + { + var sut = new ComponentParameterCollection + { + ComponentParameter.CreateParameter("attr", "") + }; + + var rf = sut.ToRenderFragment(); + + var c = RenderWithRenderFragment(rf).Instance; + c.Attributes?.ContainsKey("attr").ShouldBeTrue(); + } + + [Fact(DisplayName = "ToComponentRenderFragment passes EventCallback params to component in RenderFragment")] + public void Test017() + { + // arrange + var ec1 = EventCallback.Factory.Create(this, () => { }); + var ec2 = EventCallback.Factory.Create(this, () => { }); + var ec3 = EventCallback.Factory.Create(this, (e) => { }); + var ec4 = EventCallback.Factory.Create(this, (e) => { }); + var sut = new ComponentParameterCollection + { + ComponentParameter.CreateParameter(nameof(Params.NullableEC), ec1), + ComponentParameter.CreateParameter(nameof(Params.EC), ec2), + ComponentParameter.CreateParameter(nameof(Params.NullableECWithArgs), ec3), + ComponentParameter.CreateParameter(nameof(Params.ECWithArgs), ec4) + }; + + // act + var rf = sut.ToRenderFragment(); + + // assert + var c = RenderWithRenderFragment(rf).Instance; + c.NullableEC.ShouldBe(ec1); + c.EC.ShouldBe(ec2); + c.NullableECWithArgs.ShouldBe(ec3); + c.ECWithArgs.ShouldBe(ec4); + } + + [Fact(DisplayName = "ToComponentRenderFragment passes single template param to component in RenderFragment")] + public void Test018() + { + var sut = new ComponentParameterCollection + { + ComponentParameter.CreateParameter(nameof(Params.Template), (RenderFragment)(s => b => b.AddMarkupContent(0, s))) + }; + + var rf = sut.ToRenderFragment(); + + var rc = RenderWithRenderFragment(rf); + rc.Markup.ShouldBe(Params.TemplateContent); + } + + [Fact(DisplayName = "ToComponentRenderFragment passes multiple template params to component in RenderFragment")] + public void Test019() + { + var sut = new ComponentParameterCollection + { + ComponentParameter.CreateParameter(nameof(Params.Template), (RenderFragment)(s => b => b.AddMarkupContent(0, $"{s}1"))), + ComponentParameter.CreateParameter(nameof(Params.Template), (RenderFragment)(s => b => b.AddMarkupContent(0, $"{s}2"))) + }; + + var rf = sut.ToRenderFragment(); + + var rc = RenderWithRenderFragment(rf); + rc.Markup.ShouldBe($"{Params.TemplateContent}1{Params.TemplateContent}2"); + } + + [Fact(DisplayName = "ToComponentRenderFragment with different template types to same param throws")] + public void Test020() + { + var sut = new ComponentParameterCollection + { + ComponentParameter.CreateParameter(nameof(Params.Template), (RenderFragment)(s => b => b.AddMarkupContent(0, $"{s}1"))), + ComponentParameter.CreateParameter(nameof(Params.Template), (RenderFragment)(s => b => b.AddMarkupContent(0, $"{s}2"))) + }; + + var rf = sut.ToRenderFragment(); + + Should.Throw(() => RenderWithRenderFragment(rf)); + } + + [Fact(DisplayName = "ToComponentRenderFragment passes skips null RenderFragment params")] + public void Test030() + { + var sut = new ComponentParameterCollection + { + ComponentParameter.CreateParameter(nameof(Params.OtherFragment), default(RenderFragment)), + ComponentParameter.CreateParameter(nameof(Params.OtherFragment), (RenderFragment)(b => b.AddMarkupContent(0, "BAR"))) + }; + + var rf = sut.ToRenderFragment(); + + var rc = RenderWithRenderFragment(rf); + rc.Markup.ShouldBe("BAR"); + } + + [Fact(DisplayName = "ToComponentRenderFragment throws if same regular param is added twice")] + public void Test040() + { + var sut = new ComponentParameterCollection + { + ComponentParameter.CreateParameter(nameof(Params.Param), "FOO"), + ComponentParameter.CreateParameter(nameof(Params.Param), "BAR") + }; + + var rf = sut.ToRenderFragment(); + + Should.Throw(() => RenderWithRenderFragment(rf)); + } + + [Fact(DisplayName = "ToComponentRenderFragment throws if same regular null value param is added twice")] + public void Test041() + { + var sut = new ComponentParameterCollection + { + ComponentParameter.CreateParameter(nameof(Params.Param), null), + ComponentParameter.CreateParameter(nameof(Params.Param), null) + }; + + var rf = sut.ToRenderFragment(); + + Should.Throw(() => RenderWithRenderFragment(rf)); + } + + [Fact(DisplayName = "ToComponentRenderFragment wraps component in unnamed cascading values in RenderFragment")] + public void Test050() + { + var sut = new ComponentParameterCollection + { + ComponentParameter.CreateCascadingValue(null, "FOO") + }; + + var rf = sut.ToRenderFragment(); + + var c = RenderWithRenderFragment(rf).Instance; + c.NullableCC.ShouldBe("FOO"); + } + + [Fact(DisplayName = "ToComponentRenderFragment wraps component in multiple unnamed cascading values in RenderFragment")] + public void Test051() + { + var sut = new ComponentParameterCollection + { + ComponentParameter.CreateCascadingValue(null, "FOO"), + ComponentParameter.CreateCascadingValue(null, 42) + }; + + var rf = sut.ToRenderFragment(); + + var c = RenderWithRenderFragment(rf).Instance; + c.NullableCC.ShouldBe("FOO"); + c.CC.ShouldBe(42); + } + + [Fact(DisplayName = "ToComponentRenderFragment throws when multiple unnamed cascading values with same type is added")] + public void Test052() + { + var sut = new ComponentParameterCollection + { + ComponentParameter.CreateCascadingValue(null, "FOO"), + ComponentParameter.CreateCascadingValue(null, 42), + ComponentParameter.CreateCascadingValue(null, "BAR"), + ComponentParameter.CreateCascadingValue(null, Array.Empty()) + }; + + Should.Throw(() => sut.ToRenderFragment()); + } + + [Fact(DisplayName = "ToComponentRenderFragment wraps component in named cascading values in RenderFragment")] + public void Test053() + { + var sut = new ComponentParameterCollection + { + ComponentParameter.CreateCascadingValue(nameof(Params.NullableNamedCC), "FOO") + }; + + var rf = sut.ToRenderFragment(); + + var c = RenderWithRenderFragment(rf).Instance; + c.NullableNamedCC.ShouldBe("FOO"); + } + + [Fact(DisplayName = "ToComponentRenderFragment wraps component in multiple named cascading values in RenderFragment")] + public void Test054() + { + var sut = new ComponentParameterCollection + { + ComponentParameter.CreateCascadingValue(nameof(Params.NullableNamedCC), "FOO"), + ComponentParameter.CreateCascadingValue(nameof(Params.NamedCC), 42), + ComponentParameter.CreateCascadingValue(nameof(Params.AnotherNamedCC), 1337) + }; + + var rf = sut.ToRenderFragment(); + + var c = RenderWithRenderFragment(rf).Instance; + c.NullableNamedCC.ShouldBe("FOO"); + c.NamedCC.ShouldBe(42); + c.AnotherNamedCC.ShouldBe(1337); + } + + [Fact(DisplayName = "ToComponentRenderFragment throws when multiple named cascading values with same name and type is added")] + public void Test055() + { + var sut = new ComponentParameterCollection + { + ComponentParameter.CreateCascadingValue(nameof(Params.NullableNamedCC), "FOO"), + ComponentParameter.CreateCascadingValue(nameof(Params.NamedCC), 42), + ComponentParameter.CreateCascadingValue(nameof(Params.AnotherNamedCC), 1337), + ComponentParameter.CreateCascadingValue(nameof(Params.NamedCC), 42) + }; + + Should.Throw(() => sut.ToRenderFragment()); + } + + private class Params : ComponentBase + { + public const string TemplateContent = "FOO"; + + [Parameter(CaptureUnmatchedValues = true)] + public IReadOnlyDictionary? Attributes { get; set; } + [Parameter] public int? NullableValueTypeParam { get; set; } + [Parameter] public int ValueTypeParam { get; set; } = -1; + [Parameter] public object? Param { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public RenderFragment? OtherFragment { get; set; } + [Parameter] public EventCallback? NullableEC { get; set; } + [Parameter] public EventCallback EC { get; set; } = EventCallback.Empty; + [Parameter] public EventCallback? NullableECWithArgs { get; set; } + [Parameter] public EventCallback ECWithArgs { get; set; } = EventCallback.Empty; + [Parameter] public RenderFragment? Template { get; set; } + [CascadingParameter] public string? NullableCC { get; set; } + [CascadingParameter] public int CC { get; set; } = -1; + [CascadingParameter(Name = nameof(NullableNamedCC))] public string? NullableNamedCC { get; set; } + [CascadingParameter(Name = nameof(NamedCC))] public int NamedCC { get; set; } = -1; + [CascadingParameter(Name = nameof(AnotherNamedCC))] public int AnotherNamedCC { get; set; } = -1; + + public void VerifyParamsHaveDefaultValues() + { + Attributes.ShouldBeNull(); + NullableValueTypeParam.ShouldBeNull(); + ValueTypeParam.ShouldBe(-1); + Param.ShouldBeNull(); + ChildContent.ShouldBeNull(); + OtherFragment.ShouldBeNull(); + NullableEC.ShouldBeNull(); + EC.ShouldBe(EventCallback.Empty); + NullableECWithArgs.ShouldBeNull(); + ECWithArgs.ShouldBe(EventCallback.Empty); + Template.ShouldBeNull(); + NullableCC.ShouldBeNull(); + CC.ShouldBe(-1); + NullableNamedCC.ShouldBeNull(); + NamedCC.ShouldBe(-1); + AnotherNamedCC.ShouldBe(-1); + } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, ChildContent); + builder.AddContent(1, OtherFragment); + if (Template != null) + builder.AddContent(2, Template(TemplateContent)); + } + } + } +} diff --git a/tests/bunit.core.tests/ComponentParameterFactoryTest.cs b/tests/bunit.core.tests/ComponentParameterFactoryTest.cs index 52590f856..ba5914282 100644 --- a/tests/bunit.core.tests/ComponentParameterFactoryTest.cs +++ b/tests/bunit.core.tests/ComponentParameterFactoryTest.cs @@ -1,6 +1,6 @@ using System; -using Bunit.TestAssets.SampleComponents; -using Bunit.TestDoubles.JSInterop; +using System.Threading.Tasks; +using Bunit.Rendering; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Rendering; using Shouldly; @@ -9,98 +9,276 @@ namespace Bunit { - public class ComponentParameterFactoryTest : TestContext + public class ComponentParameterFactoryTest { - string GetMarkupFromRenderFragment(RenderFragment renderFragment) - { - return ((IRenderedFragment)Renderer.RenderFragment(renderFragment)).Markup; - } - - - [Fact(DisplayName = "All types of parameters are correctly assigned to component on render")] - public void Test005() - { - Services.AddMockJSRuntime(); - - var cut = RenderComponent>( - ("some-unmatched-attribute", "unmatched value"), - (nameof(AllTypesOfParams.RegularParam), "some value"), - CascadingValue(42), - CascadingValue(nameof(AllTypesOfParams.NamedCascadingValue), 1337), - EventCallback(nameof(AllTypesOfParams.NonGenericCallback), () => throw new Exception("NonGenericCallback")), - EventCallback(nameof(AllTypesOfParams.GenericCallback), (EventArgs args) => throw new Exception("GenericCallback")), - ChildContent(nameof(ChildContent)), - RenderFragment(nameof(AllTypesOfParams.OtherContent), nameof(AllTypesOfParams.OtherContent)), - Template(nameof(AllTypesOfParams.ItemTemplate), (item) => (builder) => throw new Exception("ItemTemplate")) - ); - - // assert that all parameters have been set correctly - var instance = cut.Instance; - instance.Attributes["some-unmatched-attribute"].ShouldBe("unmatched value"); - instance.RegularParam.ShouldBe("some value"); - instance.UnnamedCascadingValue.ShouldBe(42); - instance.NamedCascadingValue.ShouldBe(1337); - Should.Throw(async () => await instance.NonGenericCallback.InvokeAsync(EventArgs.Empty)).Message.ShouldBe("NonGenericCallback"); - Should.Throw(async () => await instance.GenericCallback.InvokeAsync(EventArgs.Empty)).Message.ShouldBe("GenericCallback"); - - GetMarkupFromRenderFragment(instance.ChildContent!).ShouldBe(nameof(ChildContent)); - GetMarkupFromRenderFragment(instance.OtherContent!).ShouldBe(nameof(AllTypesOfParams.OtherContent)); - Should.Throw(() => instance.ItemTemplate!("")(new RenderTreeBuilder())).Message.ShouldBe("ItemTemplate"); - } - - [Fact(DisplayName = "All types of parameters are correctly assigned to component on re-render")] - public void Test002() - { - // arrange - Services.AddMockJSRuntime(); - var cut = RenderComponent>(); - - // assert that no parameters have been set initially - var instance = cut.Instance; - instance.Attributes.ShouldBeNull(); - instance.RegularParam.ShouldBeNull(); - instance.UnnamedCascadingValue.ShouldBeNull(); - instance.NamedCascadingValue.ShouldBeNull(); - instance.NonGenericCallback.HasDelegate.ShouldBeFalse(); - instance.GenericCallback.HasDelegate.ShouldBeFalse(); - instance.ChildContent.ShouldBeNull(); - instance.OtherContent.ShouldBeNull(); - instance.ItemTemplate.ShouldBeNull(); - - // act - set components params and render - cut.SetParametersAndRender( - ("some-unmatched-attribute", "unmatched value"), - (nameof(AllTypesOfParams.RegularParam), "some value"), - EventCallback(nameof(AllTypesOfParams.NonGenericCallback), () => throw new Exception("NonGenericCallback")), - EventCallback(nameof(AllTypesOfParams.GenericCallback), (EventArgs args) => throw new Exception("GenericCallback")), - ChildContent(ChildContent(nameof(ChildContent))), - RenderFragment(nameof(AllTypesOfParams.OtherContent), ChildContent(nameof(AllTypesOfParams.OtherContent))), - Template(nameof(AllTypesOfParams.ItemTemplate), (item) => (builder) => throw new Exception("ItemTemplate")) - ); - - instance.Attributes["some-unmatched-attribute"].ShouldBe("unmatched value"); - instance.RegularParam.ShouldBe("some value"); - Should.Throw(async () => await instance.NonGenericCallback.InvokeAsync(EventArgs.Empty)).Message.ShouldBe("NonGenericCallback"); - Should.Throw(async () => await instance.GenericCallback.InvokeAsync(EventArgs.Empty)).Message.ShouldBe("GenericCallback"); - GetMarkupFromRenderFragment(instance.ChildContent!).ShouldBe(nameof(ChildContent)); - GetMarkupFromRenderFragment(instance.OtherContent!).ShouldBe(nameof(AllTypesOfParams.OtherContent)); - Should.Throw(() => instance.ItemTemplate!("")(new RenderTreeBuilder())).Message.ShouldBe("ItemTemplate"); - } - - [Fact(DisplayName = "Template(name, markupFactory) helper correctly renders markup template")] - public void Test100() - { - var cut = RenderComponent>( - (nameof(SimpleWithTemplate.Data), new int[] { 1, 2 }), - Template(nameof(SimpleWithTemplate.Template), num => $"

{num}

") - ); - - var expected = RenderComponent>( - (nameof(SimpleWithTemplate.Data), new int[] { 1, 2 }), - Template(nameof(SimpleWithTemplate.Template), num => builder => builder.AddMarkupContent(0, $"

{num}

")) - ); - - cut.MarkupMatches(expected); + private const string NAME = nameof(NAME); + private const string EXPECTED = nameof(EXPECTED); + private static readonly TestContext Context = new TestContext(); + + private static IRenderedFragment RenderWithRenderFragment(RenderFragment renderFragment) + { + return (IRenderedFragment)Context.Renderer.RenderFragment(renderFragment); + } + + private string? Actual { get; set; } + + [Fact(DisplayName = "EventCallback(Action) creates parameter with provided name and callback")] + public async Task Test001() + { + Action action = () => Actual = EXPECTED; + + var cp = EventCallback(NAME, action); + + await VerifyEventCallbackParameter(cp); + } + + [Fact(DisplayName = "EventCallback(Action) creates parameter with provided name and callback")] + public async Task Test002() + { + Action action = args => Actual = EXPECTED; + + var cp = EventCallback(NAME, action); + + await VerifyEventCallbackParameter(cp); + } + + [Fact(DisplayName = "EventCallback(Func) creates parameter with provided name and callback")] + public async Task Test003() + { + Func action = () => { Actual = EXPECTED; return Task.CompletedTask; }; + + var cp = EventCallback(NAME, action); + + await VerifyEventCallbackParameter(cp); + } + + [Fact(DisplayName = "EventCallback(Func) creates parameter with provided name and callback")] + public async Task Test004() + { + Func action = args => { Actual = EXPECTED; return Task.CompletedTask; }; + + var cp = EventCallback(NAME, action); + + await VerifyEventCallbackParameter(cp); + } + + [Fact(DisplayName = "EventCallback(Action) creates parameter with provided name and callback")] + public async Task Test011() + { + Action action = () => Actual = EXPECTED; + + var cp = EventCallback(NAME, action); + + await VerifyEventCallbackParameter(cp); + } + + [Fact(DisplayName = "EventCallback(Action) creates parameter with provided name and callback")] + public async Task Test012() + { + Action action = args => Actual = EXPECTED; + + var cp = EventCallback(NAME, action); + + await VerifyEventCallbackParameter(cp); + } + + [Fact(DisplayName = "EventCallback(Func) creates parameter with provided name and callback")] + public async Task Test013() + { + Func action = () => { Actual = EXPECTED; return Task.CompletedTask; }; + + var cp = EventCallback(NAME, action); + + await VerifyEventCallbackParameter(cp); + } + + [Fact(DisplayName = "EventCallback(Func) creates parameter with provided name and callback")] + public async Task Test014() + { + Func action = args => { Actual = EXPECTED; return Task.CompletedTask; }; + + var cp = EventCallback(NAME, action); + + await VerifyEventCallbackParameter(cp); + } + + private async Task VerifyEventCallbackParameter(ComponentParameter cp) + { + cp.Name.ShouldBe(NAME); + cp.IsCascadingValue.ShouldBeFalse(); + await cp.Value.ShouldBeOfType().InvokeAsync(EventArgs.Empty); + Actual.ShouldBe(EXPECTED); + } + + private async Task VerifyEventCallbackParameter(ComponentParameter cp) + where TCallbackType : new() + { + cp.Name.ShouldBe(NAME); + cp.IsCascadingValue.ShouldBeFalse(); + await cp.Value.ShouldBeOfType>().InvokeAsync(new TCallbackType()); + Actual.ShouldBe(EXPECTED); + } + + [Fact(DisplayName = "Parameter creates a parameter with provided name and value")] + public void Test020() + { + var cp = Parameter(NAME, EXPECTED); + + cp.Name.ShouldBe(NAME); + cp.Value.ShouldBe(EXPECTED); + cp.IsCascadingValue.ShouldBeFalse(); + } + + [Fact(DisplayName = "Parameter creates a parameter with provided name and null value")] + public void Test021() + { + var cp = Parameter(NAME, null); + + cp.Name.ShouldBe(NAME); + cp.Value.ShouldBeNull(); + cp.IsCascadingValue.ShouldBeFalse(); + } + + [Fact(DisplayName = "CascadingValue(name, value) creates a named cascading value parameter with provided name and value")] + public void Test030() + { + var cp = CascadingValue(NAME, EXPECTED); + + cp.Name.ShouldBe(NAME); + cp.Value.ShouldBe(EXPECTED); + cp.IsCascadingValue.ShouldBeTrue(); + } + + [Fact(DisplayName = "CascadingValue(name, value) creates a unnamed cascading value parameter with provided name and value")] + public void Test031() + { + var cp = CascadingValue(EXPECTED); + + cp.Name.ShouldBeNull(); + cp.Value.ShouldBe(EXPECTED); + cp.IsCascadingValue.ShouldBeTrue(); + } + + [Fact(DisplayName = "ChildContent(string markup) creates a parameter with a RenderFragment that renders the provided markup")] + public void Test040() + { + var cp = ChildContent(EXPECTED); + + cp.Name.ShouldBe("ChildContent"); + cp.IsCascadingValue.ShouldBeFalse(); + var renderFragment = cp.Value.ShouldBeOfType(); + var renderedFragment = RenderWithRenderFragment(renderFragment); + renderedFragment.Markup.ShouldBe(EXPECTED); + } + + [Fact(DisplayName = "ChildContent() creates a parameter with a RenderFragment that renders a component of type TComponent")] + public void Test041() + { + var cp = ChildContent(); + + cp.Name.ShouldBe("ChildContent"); + cp.IsCascadingValue.ShouldBeFalse(); + var renderFragment = cp.Value.ShouldBeOfType(); + var renderedFragment = RenderWithRenderFragment(renderFragment); + renderedFragment.Markup.ShouldBe(nameof(TestComponent)); + } + + [Fact(DisplayName = "ChildContent(component parameters) creates a parameter with a RenderFragment that renders a component of type TComponent")] + public void Test042() + { + var cp = ChildContent((nameof(TestComponent.Input), EXPECTED)); + + cp.Name.ShouldBe("ChildContent"); + cp.IsCascadingValue.ShouldBeFalse(); + var renderFragment = cp.Value.ShouldBeOfType(); + var renderedFragment = RenderWithRenderFragment(renderFragment); + renderedFragment.Markup.ShouldBe(nameof(TestComponent) + EXPECTED); + } + + [Fact(DisplayName = "ChildContent(RenderFragment) creates a parameter with a RenderFragment passed to ChildContent")] + public void Test043() + { + var cp = ChildContent(b => b.AddMarkupContent(0, EXPECTED)); + + cp.Name.ShouldBe("ChildContent"); + cp.IsCascadingValue.ShouldBeFalse(); + var renderFragment = cp.Value.ShouldBeOfType(); + var renderedFragment = RenderWithRenderFragment(renderFragment); + renderedFragment.Markup.ShouldBe(EXPECTED); + } + + [Fact(DisplayName = "RenderFragment(name, markup) creates a parameter with a RenderFragment that renders a component of type TComponent")] + public void Test051() + { + var cp = RenderFragment(NAME, EXPECTED); + + cp.Name.ShouldBe(NAME); + cp.IsCascadingValue.ShouldBeFalse(); + var renderFragment = cp.Value.ShouldBeOfType(); + var renderedFragment = RenderWithRenderFragment(renderFragment); + renderedFragment.Markup.ShouldBe(EXPECTED); + } + + [Fact(DisplayName = "RenderFragment(name, component parameters) creates a parameter with a RenderFragment that renders a component of type TComponent")] + public void Test052() + { + var cp = RenderFragment(NAME, (nameof(TestComponent.Input), EXPECTED)); + + cp.Name.ShouldBe(NAME); + cp.IsCascadingValue.ShouldBeFalse(); + var renderFragment = cp.Value.ShouldBeOfType(); + var renderedFragment = RenderWithRenderFragment(renderFragment); + renderedFragment.Markup.ShouldBe(nameof(TestComponent) + EXPECTED); + } + + [Fact(DisplayName = "Template(string, RenderFragment) creates a parameter with a Template")] + public void Test061() + { + var cp = Template(NAME, s => b => b.AddMarkupContent(0, s)); + + cp.Name.ShouldBe(NAME); + cp.IsCascadingValue.ShouldBeFalse(); + var template = cp.Value.ShouldBeOfType>(); + var renderedFragment = RenderWithRenderFragment(template(EXPECTED)); + renderedFragment.Markup.ShouldBe(EXPECTED); + } + + [Fact(DisplayName = "Template(string, Func) creates a parameter with a Template")] + public void Test062() + { + var cp = Template(NAME, s => s); + + cp.Name.ShouldBe(NAME); + cp.IsCascadingValue.ShouldBeFalse(); + var template = cp.Value.ShouldBeOfType>(); + var renderedFragment = RenderWithRenderFragment(template(EXPECTED)); + renderedFragment.Markup.ShouldBe(EXPECTED); + } + + [Fact(DisplayName = "Template(string, Func) creates a parameter with a Template")] + public void Test063() + { + var cp = Template(NAME, value => new ComponentParameter[] { + (nameof(TestComponent.Input), value) + }); + + cp.Name.ShouldBe(NAME); + cp.IsCascadingValue.ShouldBeFalse(); + var template = cp.Value.ShouldBeOfType>(); + var renderedFragment = RenderWithRenderFragment(template(EXPECTED)); + renderedFragment.Markup.ShouldBe(nameof(TestComponent) + EXPECTED); + } + + class TestComponent : ComponentBase + { + [Parameter] public string? Input { get; set; } + [Parameter] public RenderFragment? Template { get; set; } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddMarkupContent(0, nameof(TestComponent)); + builder.AddMarkupContent(1, Input); + } } } } diff --git a/tests/bunit.core.tests/ShouldlyExtensions.cs b/tests/bunit.core.tests/ShouldlyExtensions.cs new file mode 100644 index 000000000..d0a396405 --- /dev/null +++ b/tests/bunit.core.tests/ShouldlyExtensions.cs @@ -0,0 +1,38 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Bunit.Rendering; +using Microsoft.AspNetCore.Components; +using Shouldly; + +namespace Bunit +{ + public static class ShouldlyExtensions + { + public static void ShouldSatisfyAllConditions(this T actual, params Action[] conditions) + { + var conds = conditions.Select(x => (Action)(() => x.Invoke(actual))).ToArray(); + ShouldSatisfyAllConditionsTestExtensions.ShouldSatisfyAllConditions(actual, conds); + } + + public static void ShouldBeParameter(this ComponentParameter parameter, string? name, [AllowNull] T value, bool isCascadingValue) + { + parameter.ShouldSatisfyAllConditions( + x => x.Name.ShouldBe(name), + x => x.Value.ShouldBe(value), + x => x.IsCascadingValue.ShouldBe(isCascadingValue) + ); + } + + public static T ShouldBeParameter(this ComponentParameter parameter, string? name, bool isCascadingValue) + { + parameter.ShouldSatisfyAllConditions( + x => x.Name.ShouldBe(name), + x => x.Value.ShouldBeOfType(), + x => x.Value.ShouldNotBeNull(), + x => x.IsCascadingValue.ShouldBe(isCascadingValue) + ); + return (T)parameter.Value!; + } + } +} diff --git a/tests/bunit.testassets/SampleComponents/SimpleWithTemplate.razor b/tests/bunit.testassets/SampleComponents/SimpleWithTemplate.razor index f3d8c017a..0ba4abec3 100644 --- a/tests/bunit.testassets/SampleComponents/SimpleWithTemplate.razor +++ b/tests/bunit.testassets/SampleComponents/SimpleWithTemplate.razor @@ -1,13 +1,10 @@ -@typeparam T +@typeparam T @foreach (var d in Data) { - if (Template is { }) - { - @Template(d); - } + @Template?.Invoke(d); } @code { - [Parameter] public RenderFragment? Template { get; set; } - [Parameter] public IReadOnlyList Data { get; set; } = Array.Empty(); -} \ No newline at end of file + [Parameter] public RenderFragment? Template { get; set; } + [Parameter] public IReadOnlyList Data { get; set; } = Array.Empty(); +} diff --git a/tests/bunit.web.tests/TestDoubles/Authorization/AuthorizationTest.cs b/tests/bunit.web.tests/TestDoubles/Authorization/AuthorizationTest.cs index fe11e4af7..cd7cb1bf4 100644 --- a/tests/bunit.web.tests/TestDoubles/Authorization/AuthorizationTest.cs +++ b/tests/bunit.web.tests/TestDoubles/Authorization/AuthorizationTest.cs @@ -103,7 +103,7 @@ public void Test006() // assert Assert.Equal("AuthenticationStateProvider", ex.ServiceName); - Assert.Equal("https://bunit.egilhansen.com/docs/test-doubles/faking-auth.html", ex.HelpLink); + Assert.Equal("https://bunit.egilhansen.com/docs/test-doubles/faking-auth", ex.HelpLink); } [Fact(DisplayName = "AuthorizeView with set policy with authenticated and authorized user")] diff --git a/tests/run-tests.ps1 b/tests/run-tests.ps1 index 95e7ff1e1..fee0df7b2 100644 --- a/tests/run-tests.ps1 +++ b/tests/run-tests.ps1 @@ -10,10 +10,13 @@ for ($num = 1 ; $num -le $maxRuns ; $num++) if($filter) { - dotnet test ..\bunit.sln -c $mode --no-restore --no-build --blame-hang --blame-hang-timeout 100s -f net5.0 --nologo --filter $filter + dotnet test .\bunit.core.tests\bunit.core.tests.csproj -c $mode --no-restore --no-build --blame-hang --blame-hang-timeout 100s -f net5.0 --nologo --filter $filter + dotnet test .\bunit.web.tests\bunit.web.tests.csproj -c $mode --no-restore --no-build --blame-hang --blame-hang-timeout 100s -f net5.0 --nologo --filter $filter + dotnet test .\bunit.xunit.tests\bunit.xunit.tests.csproj -c $mode --no-restore --no-build --blame-hang --blame-hang-timeout 100s -f net5.0 --nologo --filter $filter } else { - dotnet test ..\bunit.sln -c $mode --no-restore --no-build --blame-hang --blame-hang-timeout 100s -f net5.0 --nologo + dotnet test .\bunit.web.tests\bunit.web.tests.csproj -c $mode --no-restore --no-build --blame-hang --blame-hang-timeout 100s -f net5.0 --nologo + dotnet test .\bunit.xunit.tests\bunit.xunit.tests.csproj -c $mode --no-restore --no-build --blame-hang --blame-hang-timeout 100s -f net5.0 --nologo } }