Skip to content

Commit 10fd472

Browse files
committed
Merge branch 'dev' into xunit-split-off
# Conflicts: # .editorconfig # src/ITestContext.cs # src/TestContext.cs # src/bunit.core/Extensions/ComponentParameterExtensions.cs # src/bunit.web.tests/Builders/ComponentParameterBuilderTests.cs
2 parents 122f9f5 + 2dbad2e commit 10fd472

6 files changed

Lines changed: 1020 additions & 0 deletions

File tree

src/Builders/ComponentParameterBuilder.cs

Lines changed: 395 additions & 0 deletions
Large diffs are not rendered by default.

src/ITestContext.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System;
2+
using AngleSharp.Dom;
3+
using Microsoft.AspNetCore.Components;
4+
5+
namespace Bunit
6+
{
7+
/// <summary>
8+
/// A test context is a factory that makes it possible to create components under tests.
9+
/// </summary>
10+
public interface ITestContext : IDisposable
11+
{
12+
/// <summary>
13+
/// Gets the service collection and service provider that is used when a
14+
/// component is rendered by the test context.
15+
/// </summary>
16+
TestServiceProvider Services { get; }
17+
18+
/// <summary>
19+
/// Gets the renderer used to render the components and fragments in this test context.
20+
/// </summary>
21+
TestRenderer Renderer { get; }
22+
23+
/// <summary>
24+
/// Parses a markup HTML string using the AngleSharps HTML5 parser
25+
/// and returns a list of nodes.
26+
/// </summary>
27+
/// <param name="markup">The markup to parse.</param>
28+
/// <returns>The <see cref="INodeList"/>.</returns>
29+
INodeList CreateNodes(string markup);
30+
31+
/// <summary>
32+
/// Instantiates and performs a first render of a component of type <typeparamref name="TComponent"/>.
33+
/// </summary>
34+
/// <typeparam name="TComponent">Type of the component to render</typeparam>
35+
/// <param name="parameters">Parameters to pass to the component when it is rendered</param>
36+
/// <returns>The rendered <typeparamref name="TComponent"/></returns>
37+
IRenderedComponent<TComponent> RenderComponent<TComponent>(params ComponentParameter[] parameters) where TComponent : class, IComponent;
38+
39+
/// <summary>
40+
/// Instantiates and performs a first render of a component of type <typeparamref name="TComponent"/>.
41+
/// </summary>
42+
/// <typeparam name="TComponent">Type of the component to render</typeparam>
43+
/// <param name="componentParameterBuilderAction">The ComponentParameterBuilder action to add type safe parameters to pass to the component when it is rendered</param>
44+
/// <returns>The rendered <typeparamref name="TComponent"/></returns>
45+
IRenderedComponent<TComponent> RenderComponent<TComponent>(Action<ComponentParameterBuilder<TComponent>> componentParameterBuilderAction) where TComponent : class, IComponent;
46+
}
47+
}

src/TestContext.cs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
using AngleSharp.Dom;
2+
using Bunit.Diffing;
3+
using Bunit.Mocking.JSInterop;
4+
using Microsoft.AspNetCore.Components;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using Microsoft.Extensions.Logging;
7+
using Microsoft.Extensions.Logging.Abstractions;
8+
using Microsoft.JSInterop;
9+
using System;
10+
using System.Linq;
11+
12+
namespace Bunit
13+
{
14+
/// <summary>
15+
/// A test context is a factory that makes it possible to create components under tests.
16+
/// </summary>
17+
public class TestContext : ITestContext, IDisposable
18+
{
19+
private readonly Lazy<TestRenderer> _renderer;
20+
private readonly Lazy<TestHtmlParser> _htmlParser;
21+
22+
/// <inheritdoc/>
23+
public virtual TestRenderer Renderer => _renderer.Value;
24+
25+
/// <inheritdoc/>
26+
public virtual TestServiceProvider Services { get; } = new TestServiceProvider();
27+
28+
/// <summary>
29+
/// Creates a new instance of the <see cref="TestContext"/> class.
30+
/// </summary>
31+
public TestContext()
32+
{
33+
Services.AddSingleton<IJSRuntime>(new PlaceholderJsRuntime());
34+
35+
_renderer = new Lazy<TestRenderer>(() =>
36+
{
37+
var loggerFactory = Services.GetService<ILoggerFactory>() ?? NullLoggerFactory.Instance;
38+
return new TestRenderer(Services, loggerFactory);
39+
});
40+
_htmlParser = new Lazy<TestHtmlParser>(() =>
41+
{
42+
return new TestHtmlParser(Renderer, new HtmlComparer());
43+
});
44+
}
45+
46+
/// <inheritdoc/>
47+
public virtual INodeList CreateNodes(string markup)
48+
=> _htmlParser.Value.Parse(markup);
49+
50+
/// <inheritdoc/>
51+
public virtual IRenderedComponent<TComponent> RenderComponent<TComponent>(params ComponentParameter[] parameters) where TComponent : class, IComponent
52+
{
53+
var result = new RenderedComponent<TComponent>(this, parameters);
54+
return result;
55+
}
56+
57+
/// <inheritdoc/>
58+
public virtual IRenderedComponent<TComponent> RenderComponent<TComponent>(Action<ComponentParameterBuilder<TComponent>> componentParameterBuilderAction) where TComponent : class, IComponent
59+
{
60+
if (componentParameterBuilderAction is null)
61+
{
62+
throw new ArgumentNullException(nameof(componentParameterBuilderAction));
63+
}
64+
65+
var builder = new ComponentParameterBuilder<TComponent>();
66+
componentParameterBuilderAction(builder);
67+
68+
return RenderComponent<TComponent>(builder.Build().ToArray());
69+
}
70+
71+
#region IDisposable Support
72+
private bool _disposed = false;
73+
74+
/// <inheritdoc/>
75+
protected virtual void Dispose(bool disposing)
76+
{
77+
if (!_disposed)
78+
{
79+
if (disposing)
80+
{
81+
if (_renderer.IsValueCreated)
82+
_renderer.Value.Dispose();
83+
if (_htmlParser.IsValueCreated)
84+
_htmlParser.Value.Dispose();
85+
86+
Services.Dispose();
87+
}
88+
_disposed = true;
89+
}
90+
}
91+
92+
/// <inheritdoc/>
93+
public void Dispose()
94+
{
95+
Dispose(true);
96+
GC.SuppressFinalize(this);
97+
}
98+
#endregion
99+
}
100+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using Microsoft.AspNetCore.Components;
5+
6+
namespace Bunit
7+
{
8+
/// <summary>
9+
/// Helpful extensions for working with <see cref="ComponentParameter"/> and collections of these.
10+
/// </summary>
11+
internal static class ComponentParameterExtensions
12+
{
13+
/// <summary>
14+
/// Creates a <see cref="RenderFragment"/> that will render a component of <typeparamref name="TComponent"/> type,
15+
/// with the provided <paramref name="parameters"/>. If one or more of the <paramref name="parameters"/> include
16+
/// a cascading values, the <typeparamref name="TComponent"/> will be wrapped in <see cref="Microsoft.AspNetCore.Components.CascadingValue{TValue}"/>
17+
/// components.
18+
/// </summary>
19+
/// <typeparam name="TComponent">Type of component to render in the render fragment</typeparam>
20+
/// <param name="parameters">Parameters to pass to the component</param>
21+
/// <returns>The <see cref="RenderFragment"/>.</returns>
22+
public static RenderFragment ToComponentRenderFragment<TComponent>(this IReadOnlyList<ComponentParameter> parameters) where TComponent : class, IComponent
23+
{
24+
var cascadingParams = new Queue<ComponentParameter>(parameters.Where(x => x.IsCascadingValue));
25+
26+
if (cascadingParams.Count > 0)
27+
return CreateCascadingValueRenderFragment(cascadingParams, parameters);
28+
else
29+
return CreateComponentRenderFragment(parameters);
30+
31+
static RenderFragment CreateCascadingValueRenderFragment(Queue<ComponentParameter> cascadingParams, IReadOnlyList<ComponentParameter> parameters)
32+
{
33+
var cp = cascadingParams.Dequeue();
34+
var cascadingValueType = GetCascadingValueType(cp);
35+
return builder =>
36+
{
37+
builder.OpenComponent(0, cascadingValueType);
38+
if (cp.Name is { })
39+
builder.AddAttribute(1, nameof(CascadingValue<object>.Name), cp.Name);
40+
41+
builder.AddAttribute(2, nameof(CascadingValue<object>.Value), cp.Value);
42+
builder.AddAttribute(3, nameof(CascadingValue<object>.IsFixed), true);
43+
44+
if (cascadingParams.Count > 0)
45+
builder.AddAttribute(4, nameof(CascadingValue<object>.ChildContent), CreateCascadingValueRenderFragment(cascadingParams, parameters));
46+
else
47+
builder.AddAttribute(4, nameof(CascadingValue<object>.ChildContent), CreateComponentRenderFragment(parameters));
48+
49+
builder.CloseComponent();
50+
};
51+
}
52+
53+
static RenderFragment CreateComponentRenderFragment(IReadOnlyList<ComponentParameter> parameters)
54+
{
55+
return builder =>
56+
{
57+
builder.OpenComponent(0, typeof(TComponent));
58+
59+
foreach (var parameterValue in parameters.Where(x => !x.IsCascadingValue))
60+
builder.AddAttribute(1, parameterValue.Name, parameterValue.Value);
61+
62+
builder.CloseComponent();
63+
};
64+
}
65+
}
66+
67+
private static readonly Type CascadingValueType = typeof(CascadingValue<>);
68+
69+
private static Type GetCascadingValueType(ComponentParameter parameter)
70+
{
71+
if (parameter.Value is null) throw new InvalidOperationException("Cannot get the type of a null object");
72+
var cascadingValueType = parameter.Value.GetType();
73+
return CascadingValueType.MakeGenericType(cascadingValueType);
74+
}
75+
}
76+
}

0 commit comments

Comments
 (0)