Skip to content

Commit 8bab3be

Browse files
committed
refactor: moved stub component to core, allowed passing template in that replaces stubbed component
1 parent db16651 commit 8bab3be

17 files changed

Lines changed: 472 additions & 428 deletions

src/bunit.core/ComponentFactories/ComponentFactoryCollectionCoreExtensions.cs

Lines changed: 0 additions & 34 deletions
This file was deleted.

src/bunit.web/ComponentFactories/StubComponentFactory.cs renamed to src/bunit.core/ComponentFactories/StubComponentFactory.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,32 @@
11
#if NET5_0_OR_GREATER
22
using System;
3+
using System.Collections.Generic;
34
using Bunit.TestDoubles;
45
using Microsoft.AspNetCore.Components;
56

67
namespace Bunit.ComponentFactories
78
{
89
internal sealed class StubComponentFactory : IComponentFactory
910
{
11+
private static readonly Type StubType = typeof(Stub<>);
12+
1013
private readonly Predicate<Type> componentTypePredicate;
11-
private readonly StubOptions options;
14+
private readonly RenderFragment<IReadOnlyDictionary<string, object>>? replacementTemplate;
1215

13-
public StubComponentFactory(Predicate<Type> componentTypePredicate, StubOptions options)
16+
public StubComponentFactory(Predicate<Type> componentTypePredicate, RenderFragment<IReadOnlyDictionary<string, object>>? replacementTemplate = null)
1417
{
1518
this.componentTypePredicate = componentTypePredicate;
16-
this.options = options;
19+
this.replacementTemplate = replacementTemplate;
1720
}
1821

1922
public bool CanCreate(Type componentType)
2023
=> componentTypePredicate.Invoke(componentType);
2124

2225
public IComponent Create(Type componentType)
23-
=> ComponentDoubleFactory.CreateStub(componentType, options);
26+
{
27+
var typeToCreate = StubType.MakeGenericType(componentType);
28+
return (IComponent)Activator.CreateInstance(typeToCreate, new object?[] { replacementTemplate })!;
29+
}
2430
}
2531
}
2632
#endif
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
#if NET5_0_OR_GREATER
2+
using System;
3+
using System.Collections.Generic;
4+
using Bunit.ComponentFactories;
5+
using Bunit.TestDoubles;
6+
using Microsoft.AspNetCore.Components;
7+
8+
namespace Bunit
9+
{
10+
/// <summary>
11+
/// Extension methods for using component doubles.
12+
/// </summary>
13+
public static class ComponentFactoryCollectionExtensions
14+
{
15+
private static readonly RenderFragment<IReadOnlyDictionary<string, object>> NoopReplacementTemplate = p => b => {};
16+
17+
/// <summary>
18+
/// Configures bUnit to replace all components of type <typeparamref name="TComponent"/> with a component
19+
/// of type <typeparamref name="TReplacementComponent"/>.
20+
/// </summary>
21+
/// <typeparam name="TComponent">Type of component to replace.</typeparam>
22+
/// <typeparam name="TReplacementComponent">Type of component to replace with.</typeparam>
23+
/// <param name="factories">The bUnit <see cref="ComponentFactoryCollection"/> to configure.</param>
24+
/// <returns>A <see cref="ComponentFactoryCollection"/>.</returns>
25+
public static ComponentFactoryCollection Add<TComponent, TReplacementComponent>(this ComponentFactoryCollection factories)
26+
where TComponent : IComponent
27+
where TReplacementComponent : IComponent
28+
{
29+
if (factories is null)
30+
throw new ArgumentNullException(nameof(factories));
31+
32+
factories.Add(new GenericComponentFactory<TComponent, TReplacementComponent>());
33+
34+
return factories;
35+
}
36+
37+
/// <summary>
38+
/// Configures bUnit to use replace all components of type <typeparamref name="TComponent"/> (including derived components)
39+
/// with a <see cref="Stub{TComponent}"/> component in the render tree.
40+
/// </summary>
41+
/// <remarks>NOTE: This will replace any component of type <typeparamref name="TComponent"/> or components that derives/inherits from it.</remarks>
42+
/// <typeparam name="TComponent">The type of component to replace with a <see cref="Stub{TComponent}"/> component.</typeparam>
43+
/// <param name="factories">The bUnit <see cref="ComponentFactoryCollection"/> to configure.</param>
44+
/// <returns>A <see cref="ComponentFactoryCollection"/>.</returns>
45+
public static ComponentFactoryCollection AddStub<TComponent>(this ComponentFactoryCollection factories) where TComponent : IComponent
46+
=> AddStub<TComponent>(factories, NoopReplacementTemplate);
47+
48+
/// <summary>
49+
/// Configures bUnit to use replace all components of type <typeparamref name="TComponent"/> (including derived components)
50+
/// with a <see cref="Stub{TComponent}"/> component in the render tree.
51+
/// </summary>
52+
/// <remarks>NOTE: This will replace any component of type <typeparamref name="TComponent"/> or components that derives/inherits from it.</remarks>
53+
/// <typeparam name="TComponent">The type of component to replace with a <see cref="Stub{TComponent}"/> component.</typeparam>
54+
/// <param name="factories">The bUnit <see cref="ComponentFactoryCollection"/> to configure.</param>
55+
/// <param name="replacementTemplate">Optional replacement template that will be used to render output instead of the stubbed out component.</param>
56+
/// <returns>A <see cref="ComponentFactoryCollection"/>.</returns>
57+
public static ComponentFactoryCollection AddStub<TComponent>(
58+
this ComponentFactoryCollection factories,
59+
Func<IReadOnlyDictionary<string, object>, string> replacementTemplate)
60+
where TComponent : IComponent
61+
{
62+
return AddStub<TComponent>(
63+
factories,
64+
ps => b => b.AddMarkupContent(0, replacementTemplate(ps)));
65+
}
66+
67+
/// <summary>
68+
/// Configures bUnit to use replace all components of type <typeparamref name="TComponent"/> (including derived components)
69+
/// with a <see cref="Stub{TComponent}"/> component in the render tree.
70+
/// </summary>
71+
/// <remarks>NOTE: This will replace any component of type <typeparamref name="TComponent"/> or components that derives/inherits from it.</remarks>
72+
/// <typeparam name="TComponent">The type of component to replace with a <see cref="Stub{TComponent}"/> component.</typeparam>
73+
/// <param name="factories">The bUnit <see cref="ComponentFactoryCollection"/> to configure.</param>
74+
/// <param name="replacementTemplate">Optional replacement template that will be used to render output instead of the stubbed out component.</param>
75+
/// <returns>A <see cref="ComponentFactoryCollection"/>.</returns>
76+
public static ComponentFactoryCollection AddStub<TComponent>(
77+
this ComponentFactoryCollection factories,
78+
RenderFragment<IReadOnlyDictionary<string, object>> replacementTemplate)
79+
where TComponent : IComponent
80+
{
81+
return AddStub(factories, CreatePredicate(typeof(TComponent)), replacementTemplate);
82+
83+
static Predicate<Type> CreatePredicate(Type componentTypeToStub)
84+
=> componentType => componentType == componentTypeToStub || componentType.IsAssignableTo(componentTypeToStub);
85+
}
86+
87+
/// <summary>
88+
/// Configures bUnit to use replace all components whose type make the <paramref name="componentTypePredicate"/> predicate return <c>true</c>
89+
/// with a <see cref="Stub{TComponent}"/> component in the render tree.
90+
/// </summary>
91+
/// <param name="factories">The bUnit <see cref="ComponentFactoryCollection"/> to configure.</param>
92+
/// <param name="componentTypePredicate">The predicate which decides if a component should be replaced with a <see cref="Stub{TComponent}"/> component.</param>
93+
/// <returns>A <see cref="ComponentFactoryCollection"/>.</returns>
94+
public static ComponentFactoryCollection AddStub(
95+
this ComponentFactoryCollection factories,
96+
Predicate<Type> componentTypePredicate)
97+
=> AddStub(
98+
factories,
99+
componentTypePredicate,
100+
NoopReplacementTemplate);
101+
102+
/// <summary>
103+
/// Configures bUnit to use replace all components whose type make the <paramref name="componentTypePredicate"/> predicate return <c>true</c>
104+
/// with a <see cref="Stub{TComponent}"/> component in the render tree.
105+
/// </summary>
106+
/// <param name="factories">The bUnit <see cref="ComponentFactoryCollection"/> to configure.</param>
107+
/// <param name="componentTypePredicate">The predicate which decides if a component should be replaced with a <see cref="Stub{TComponent}"/> component.</param>
108+
/// <param name="replacementTemplate">Optional replacement template that will be used to render output instead of the stubbed out component.</param>
109+
/// <returns>A <see cref="ComponentFactoryCollection"/>.</returns>
110+
public static ComponentFactoryCollection AddStub(
111+
this ComponentFactoryCollection factories,
112+
Predicate<Type> componentTypePredicate,
113+
Func<IReadOnlyDictionary<string, object>, string> replacementTemplate)
114+
=> AddStub(
115+
factories,
116+
componentTypePredicate,
117+
ps => b => b.AddMarkupContent(0, replacementTemplate(ps)));
118+
119+
/// <summary>
120+
/// Configures bUnit to use replace all components whose type make the <paramref name="componentTypePredicate"/> predicate return <c>true</c>
121+
/// with a <see cref="Stub{TComponent}"/> component in the render tree.
122+
/// </summary>
123+
/// <param name="factories">The bUnit <see cref="ComponentFactoryCollection"/> to configure.</param>
124+
/// <param name="componentTypePredicate">The predicate which decides if a component should be replaced with a <see cref="Stub{TComponent}"/> component.</param>
125+
/// <param name="replacementTemplate">Optional replacement template that will be used to render output instead of the stubbed out component.</param>
126+
/// <returns>A <see cref="ComponentFactoryCollection"/>.</returns>
127+
public static ComponentFactoryCollection AddStub(
128+
this ComponentFactoryCollection factories,
129+
Predicate<Type> componentTypePredicate,
130+
RenderFragment<IReadOnlyDictionary<string, object>> replacementTemplate)
131+
{
132+
if (factories is null)
133+
throw new ArgumentNullException(nameof(factories));
134+
if (componentTypePredicate is null)
135+
throw new ArgumentNullException(nameof(componentTypePredicate));
136+
137+
factories.Add(new StubComponentFactory(componentTypePredicate, replacementTemplate));
138+
return factories;
139+
}
140+
}
141+
}
142+
#endif
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
#if NET5_0_OR_GREATER
2+
using System;
3+
using System.Collections;
4+
using System.Collections.Generic;
5+
using System.Collections.Immutable;
6+
using System.Diagnostics.CodeAnalysis;
7+
using System.Linq.Expressions;
8+
using System.Reflection;
9+
using Microsoft.AspNetCore.Components;
10+
11+
namespace Bunit.TestDoubles
12+
{
13+
/// <summary>
14+
/// Represents a view of parameters captured by a <see cref="ComponentDoubleBase{TComponent}"/>.
15+
/// </summary>
16+
/// <typeparam name="TComponent"></typeparam>
17+
[SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix", Justification = "Using Blazor naming conventions.")]
18+
public class CapturedParameterView<TComponent> : IReadOnlyDictionary<string, object>
19+
where TComponent : IComponent
20+
{
21+
/// <summary>
22+
/// Gets a empty <see cref="CapturedParameterView{TComponent}"/>.
23+
/// </summary>
24+
public static CapturedParameterView<TComponent> Empty { get; } = new(ImmutableDictionary<string, object>.Empty);
25+
26+
private static readonly Type ComponentType = typeof(TComponent);
27+
28+
private readonly IReadOnlyDictionary<string, object> parameters;
29+
30+
private CapturedParameterView(IReadOnlyDictionary<string, object> parameters)
31+
=> this.parameters = parameters;
32+
33+
/// <summary>
34+
/// Gets the value of the parameter with the <paramref name="key"/>.
35+
/// </summary>
36+
/// <param name="key">Name of the parameter to get.</param>
37+
/// <returns>The value of the parameter</returns>
38+
public object this[string key]
39+
=> parameters[key];
40+
41+
/// <inheritdoc/>
42+
public IEnumerable<string> Keys
43+
=> parameters.Keys;
44+
45+
/// <inheritdoc/>
46+
public IEnumerable<object> Values
47+
=> parameters.Values;
48+
49+
/// <inheritdoc/>
50+
public int Count
51+
=> parameters.Count;
52+
53+
/// <inheritdoc/>
54+
public bool ContainsKey(string key)
55+
=> parameters.ContainsKey(key);
56+
57+
/// <inheritdoc/>
58+
public bool TryGetValue(string key, [MaybeNullWhen(false)] out object value)
59+
=> parameters.TryGetValue(key, out value);
60+
61+
/// <summary>
62+
/// Gets the value of a parameter passed to the captured <typeparamref name="TComponent"/>,
63+
/// using the <paramref name="parameterSelector"/>.
64+
/// </summary>
65+
/// <typeparam name="TValue">The type of the parameter to find.</typeparam>
66+
/// <param name="parameterSelector">A parameter selector that selects the parameter property of <typeparamref name="TComponent"/>.</param>
67+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="parameterSelector"/> is null.</exception>
68+
/// <exception cref="ArgumentException">Thrown when the member of <typeparamref name="TComponent"/> selected by the <paramref name="parameterSelector"/> is not a Blazor parameter.</exception>
69+
/// <exception cref="ParameterNotFoundException">Thrown when the selected parameter was not passed to the captured <typeparamref name="TComponent"/>.</exception>
70+
/// <exception cref="InvalidCastException">Throw when the type of the value passed to the selected parameter is not the same as the selected parameters type, i.e. <typeparamref name="TValue"/>.</exception>
71+
/// <returns>The <typeparamref name="TValue"/>.</returns>
72+
public TValue Get<TValue>(Expression<Func<TComponent, TValue>> parameterSelector)
73+
{
74+
if (parameterSelector is null)
75+
throw new ArgumentNullException(nameof(parameterSelector));
76+
77+
if (!(parameterSelector.Body is MemberExpression memberExpression) || !(memberExpression.Member is PropertyInfo propInfoCandidate))
78+
throw new ArgumentException($"The parameter selector '{parameterSelector}' does not resolve to a public property on the component '{typeof(TComponent)}'.", nameof(parameterSelector));
79+
80+
var propertyInfo = propInfoCandidate.DeclaringType != ComponentType
81+
? ComponentType.GetProperty(propInfoCandidate.Name, propInfoCandidate.PropertyType)
82+
: propInfoCandidate;
83+
84+
var paramAttr = propertyInfo?.GetCustomAttribute<ParameterAttribute>(inherit: true);
85+
var cascadingParamAttr = propertyInfo?.GetCustomAttribute<CascadingParameterAttribute>(inherit: true);
86+
87+
if (propertyInfo is null || (paramAttr is null && cascadingParamAttr is null))
88+
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.", nameof(parameterSelector));
89+
90+
if (!parameters.TryGetValue(propertyInfo.Name, out var objectResult))
91+
throw new ParameterNotFoundException(propertyInfo.Name, ComponentType.ToString());
92+
93+
return (TValue)objectResult;
94+
}
95+
96+
/// <inheritdoc/>
97+
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
98+
=> parameters.GetEnumerator();
99+
100+
/// <inheritdoc/>
101+
IEnumerator IEnumerable.GetEnumerator()
102+
=> ((IEnumerable)parameters).GetEnumerator();
103+
104+
/// <summary>
105+
/// Create an instances of the <see cref="CapturedParameterView{TComponent}"/>
106+
/// from the <paramref name="parameters"/> <see cref="ParameterView"/>.
107+
/// </summary>
108+
/// <param name="parameters">Parameters to create from.</param>
109+
/// <returns>An instance of <see cref="CapturedParameterView{TComponent}"/>.</returns>
110+
[SuppressMessage("Design", "CA1000:Do not declare static members on generic types", Justification = "Following factory pattern used in .net")]
111+
public static CapturedParameterView<TComponent> From(ParameterView parameters)
112+
=> new(parameters.ToDictionary());
113+
}
114+
}
115+
#endif

0 commit comments

Comments
 (0)