Skip to content

Commit 0e396ef

Browse files
committed
feat: ComponentFactories.Add<T>(instance)
1 parent 095e6a7 commit 0e396ef

6 files changed

Lines changed: 144 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ All notable changes to **bUnit** will be documented in this file. The project ad
66

77
## [Unreleased]
88

9+
### Added
10+
11+
- Add `ComponentFactories` extensions method that makes it easy to register an instance of a replacement component. By [@egil](https://github.com/egil).
12+
913
### Fixed
1014

1115
- Changed `SetParametersAndRender` such that it rethrows any exceptions thrown by the component under tests `SetParametersAsync` method. Thanks to [@bonsall](https://github.com/bonsall) for reporting the issue. Fixed by [@egil](https://github.com/egil).
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#if NET5_0_OR_GREATER
2+
using System;
3+
using Microsoft.AspNetCore.Components;
4+
5+
namespace Bunit.ComponentFactories
6+
{
7+
internal sealed class InstanceComponentFactory<TComponent> : IComponentFactory
8+
where TComponent : IComponent
9+
{
10+
private readonly TComponent instance;
11+
private int createCount;
12+
13+
public InstanceComponentFactory(TComponent instance)
14+
=> this.instance = instance;
15+
16+
public bool CanCreate(Type componentType)
17+
=> componentType == typeof(TComponent);
18+
19+
public IComponent Create(Type componentType)
20+
{
21+
if(createCount == 1)
22+
{
23+
throw new InvalidOperationException(
24+
$"The instance object passed to the" +
25+
$"{nameof(TestContextBase.ComponentFactories)}.{nameof(ComponentFactoryCollectionExtensions.Add)}<{typeof(TComponent).Name}>(instance) method can only be used to replace " +
26+
$"one {typeof(TComponent)} component in the render tree.");
27+
}
28+
29+
createCount++;
30+
31+
return instance;
32+
}
33+
}
34+
}
35+
#endif

src/bunit.core/Extensions/ComponentFactoryCollectionExtensions.cs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,33 @@ public static ComponentFactoryCollection Add<TComponent, TReplacementComponent>(
2828
factories.Add(new GenericComponentFactory<TComponent, TReplacementComponent>());
2929

3030
return factories;
31-
}
31+
}
32+
33+
/// <summary>
34+
/// Configures bUnit to replace a <typeparamref name="TComponent"/> component in the render tree
35+
/// with the provided <paramref name="instance"/>.
36+
/// </summary>
37+
/// <remarks>
38+
/// Only one <typeparamref name="TComponent"/> component can be replaced with the replacement component (<paramref name="instance"/>).
39+
/// If there are two or more <typeparamref name="TComponent"/> components in the render tree, an exception is thrown.
40+
/// </remarks>
41+
/// <typeparam name="TComponent">Type of component to replace.</typeparam>
42+
/// <param name="factories">The bUnit <see cref="ComponentFactoryCollection"/> to configure.</param>
43+
/// <param name="instance">The instance of the replacement component.</param>
44+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="factories"/> and/or <paramref name="instance"/> is null.</exception>
45+
/// <returns>A <see cref="ComponentFactoryCollection"/>.</returns>
46+
public static ComponentFactoryCollection Add<TComponent>(this ComponentFactoryCollection factories, TComponent instance)
47+
where TComponent : IComponent
48+
{
49+
if (factories is null)
50+
throw new ArgumentNullException(nameof(factories));
51+
if (instance is null)
52+
throw new ArgumentNullException(nameof(instance));
53+
54+
factories.Add(new InstanceComponentFactory<TComponent>(instance));
55+
56+
return factories;
57+
}
3258
}
3359
}
3460
#endif

tests/bunit.core.tests/ComponentFactories/GenericComponentFactoryTest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ namespace Bunit.ComponentFactories
1111
{
1212
public class GenericComponentFactoryTest : TestContext
1313
{
14-
[Fact(DisplayName = "UseFor throws when factories is null")]
14+
[Fact(DisplayName = "Add throws when factories is null")]
1515
public void Test001()
1616
=> Should.Throw<ArgumentNullException>(() => ComponentFactoryCollectionExtensions.Add<Simple1, FakeSimple1>(factories: default));
1717

18-
[Fact(DisplayName = "UseFor<TComponent, TReplacementComponent> replaces components of type TComponent with TReplacementComponent")]
18+
[Fact(DisplayName = "Add<TComponent, TReplacementComponent> replaces components of type TComponent with TReplacementComponent")]
1919
public void Test002()
2020
{
2121
ComponentFactories.Add<Simple1, FakeSimple1>();
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#if NET5_0_OR_GREATER
2+
using System;
3+
using Bunit.TestAssets.SampleComponents;
4+
using Moq;
5+
using Shouldly;
6+
using Xunit;
7+
8+
namespace Bunit.ComponentFactories
9+
{
10+
public class InstanceComponentFactoryTest : TestContext
11+
{
12+
[Fact(DisplayName = "Add throws when factories is null")]
13+
public void Test001()
14+
=> Should.Throw<ArgumentNullException>(() => ComponentFactoryCollectionExtensions.Add<Simple1>(default, default));
15+
16+
[Fact(DisplayName = "Add throws when instance is null")]
17+
public void Test002()
18+
=> Should.Throw<ArgumentNullException>(() => ComponentFactories.Add<Simple1>(default));
19+
20+
[Fact(DisplayName = "Factory replaces one TComponent with instance in the render tree")]
21+
public void Test010()
22+
{
23+
var simple1Mock = new Mock<Simple1>();
24+
ComponentFactories.Add<Simple1>(simple1Mock.Object);
25+
26+
var cut = RenderComponent<Wrapper>(ps => ps.AddChildContent<Simple1>());
27+
28+
cut.FindComponent<Simple1>().Instance
29+
.ShouldBeSameAs(simple1Mock.Object);
30+
}
31+
32+
[Fact(DisplayName = "Factory throws if component instance is requested twice for TComponent that inherits from ComponentBase")]
33+
public void Test020()
34+
{
35+
var simple1Mock = new Mock<Simple1>();
36+
ComponentFactories.Add<Simple1>(simple1Mock.Object);
37+
38+
Should.Throw<InvalidOperationException>(() => RenderComponent<TwoComponentWrapper>(ps => ps
39+
.Add<Simple1>(p => p.First)
40+
.Add<Simple1>(p => p.Second)));
41+
}
42+
43+
[Fact(DisplayName = "Factory throws if component instance is requested twice for TComponent that implements from IComponent")]
44+
public void Test021()
45+
{
46+
var simple1Mock = new Mock<BasicComponent>();
47+
ComponentFactories.Add<BasicComponent>(simple1Mock.Object);
48+
49+
Should.Throw<InvalidOperationException>(() => RenderComponent<TwoComponentWrapper>(ps => ps
50+
.Add<BasicComponent>(p => p.First)
51+
.Add<BasicComponent>(p => p.Second)));
52+
}
53+
}
54+
}
55+
#endif
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System.Threading.Tasks;
2+
using Microsoft.AspNetCore.Components;
3+
4+
namespace Bunit.TestAssets.SampleComponents
5+
{
6+
public class BasicComponent : IComponent
7+
{
8+
private RenderHandle renderHandle;
9+
10+
public void Attach(RenderHandle renderHandle)
11+
{
12+
this.renderHandle = renderHandle;
13+
}
14+
15+
public Task SetParametersAsync(ParameterView parameters)
16+
{
17+
renderHandle.Render(builder => builder.AddMarkupContent(0, nameof(BasicComponent)));
18+
return Task.CompletedTask;
19+
}
20+
}
21+
}

0 commit comments

Comments
 (0)