Skip to content

Commit e7700ef

Browse files
committed
Fixture component, TestComponentBase first draft done
1 parent 4af67eb commit e7700ef

39 files changed

+952
-383
lines changed

Directory.Build.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@
99
<WarningsAsErrors>CS8600;CS8602;CS8603;CS8625</WarningsAsErrors>
1010
</PropertyGroup>
1111

12-
<PropertyGroup>
12+
<!--<PropertyGroup>
1313
<AnnotatedReferenceAssemblyVersion>3.0.0</AnnotatedReferenceAssemblyVersion>
1414
</PropertyGroup>
1515
<ItemGroup>
1616
<PackageReference Include="TunnelVisionLabs.ReferenceAssemblyAnnotator" Version="1.0.0-alpha.90" PrivateAssets="all" />
1717
<PackageDownload Include="Microsoft.NETCore.App.Ref" Version="[$(AnnotatedReferenceAssemblyVersion)]" />
18-
</ItemGroup>
18+
</ItemGroup>-->
1919

2020
</Project>

src/AssertExtensions.cs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
4+
using System.Linq;
5+
using System.Text;
6+
using System.Threading.Tasks;
7+
using AngleSharp;
8+
using AngleSharp.Diffing.Core;
9+
using AngleSharp.Dom;
10+
using Egil.RazorComponents.Testing.Diffing;
11+
using Xunit;
12+
13+
namespace Egil.RazorComponents.Testing
14+
{
15+
public static class AssertExtensions
16+
{
17+
public static IReadOnlyList<IDiff> CompareTo(this IRenderedFragment actual, IRenderedFragment expected)
18+
{
19+
if (actual is null) throw new ArgumentNullException(nameof(actual));
20+
if (expected is null) throw new ArgumentNullException(nameof(expected));
21+
22+
return actual.GetNodes().CompareTo(expected.GetNodes());
23+
}
24+
25+
public static IReadOnlyList<IDiff> CompareTo(this INodeList actual, INodeList expected)
26+
{
27+
if (actual is null) throw new ArgumentNullException(nameof(actual));
28+
if (expected is null) throw new ArgumentNullException(nameof(expected));
29+
30+
if (actual.Length == 0 && expected.Length == 0) return Array.Empty<IDiff>();
31+
32+
var comparer = actual.Length > 0
33+
? actual[0].Owner.Context.GetService<HtmlComparer>()
34+
: expected[0].Owner.Context.GetService<HtmlComparer>();
35+
36+
return comparer.Compare(expected, actual).ToArray();
37+
}
38+
39+
public static void ShouldBeAddition(this IDiff actualChange, IRenderedFragment expectedChange, string? userMessage = null)
40+
{
41+
if (actualChange is null) throw new ArgumentNullException(nameof(actualChange));
42+
if (expectedChange is null) throw new ArgumentNullException(nameof(expectedChange));
43+
44+
var actual = Assert.IsType<UnexpectedNodeDiff>(actualChange);
45+
var expected = expectedChange.GetNodes();
46+
var comparer = expected[0].Owner.Context.GetService<HtmlComparer>();
47+
48+
var diffs = comparer.Compare(expected, new[] { actual.Test.Node }).ToList();
49+
Assert.True(diffs.Count == 0, $"{GetUserMessage()}{StringifyDiffs(expected.ToHtml(), actual.Test.Node.ToHtml(), diffs)}");
50+
51+
string GetUserMessage() => userMessage is null
52+
? $"The actual change does not match the expected change.{Environment.NewLine}"
53+
: $"{userMessage}.{Environment.NewLine}";
54+
}
55+
56+
public static void ShouldBe(this IRenderedFragment actual, IRenderedFragment expected, string? userMessage = null)
57+
{
58+
if (actual is null) throw new ArgumentNullException(nameof(actual));
59+
if (expected is null) throw new ArgumentNullException(nameof(expected));
60+
61+
actual.GetNodes().ShouldBe(expected.GetNodes(), userMessage);
62+
}
63+
64+
public static void ShouldBe(this INodeList actual, INodeList expected, string? userMessage = null)
65+
{
66+
var diffs = CompareTo(actual, expected);
67+
68+
Assert.True(diffs.Count == 0, $"{GetUserMessage()}{StringifyDiffs(expected.ToHtml(), actual.ToHtml(), diffs)}");
69+
70+
string GetUserMessage() => userMessage is null
71+
? string.Empty
72+
: $"{userMessage}.{Environment.NewLine}";
73+
}
74+
75+
[SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "<Pending>")]
76+
private static string StringifyDiffs(string controlHtml, string testHtml, IReadOnlyList<IDiff> diffs)
77+
{
78+
var diffsText = string.Join($"{Environment.NewLine}", diffs.Select((x, i) =>
79+
{
80+
var diffText = x switch
81+
{
82+
//NodeDiff diff when diff.Target == DiffTarget.Text =>
83+
// $"The control text '{diff.Control.Node.TextContent}' at {diff.Control.Path} is different from the test text '{diff.Test.Node.TextContent}' at {diff.Test.Path}.",
84+
NodeDiff diff => $"The control {diff.Control.Node.NodeType.ToString().ToLowerInvariant()} {diff.Control.Path} and the test {diff.Test.Node.NodeType.ToString().ToLowerInvariant()} {diff.Test.Path} are different.",
85+
AttrDiff diff => $"The value of the control attribute {diff.Control.Path} and test attribute {diff.Test.Path} are different.",
86+
MissingNodeDiff diff => $"The control {diff.Control.Node.NodeType.ToString().ToLowerInvariant()} at {diff.Control.Path} is missing.",
87+
MissingAttrDiff diff => $"The control attribute at {diff.Control.Path} is missing.",
88+
UnexpectedNodeDiff diff => $"The test {diff.Test.Node.NodeType.ToString().ToLowerInvariant()} at {diff.Test.Path} was not expected.",
89+
UnexpectedAttrDiff diff => $"The test attribute at {diff.Test.Path} was not expected.",
90+
_ => throw new InvalidOperationException($"Unknown diff type detected: {x.GetType()}")
91+
};
92+
return $" {i + 1}: {diffText}";
93+
}));
94+
95+
return $"{Environment.NewLine}The following errors was found during diffing: {Environment.NewLine}{diffsText}{Environment.NewLine}{Environment.NewLine}" +
96+
$"Control HTML was:{Environment.NewLine}{controlHtml}{Environment.NewLine}" +
97+
$"Test HTML was:{Environment.NewLine}{testHtml}{Environment.NewLine}";
98+
}
99+
}
100+
}
Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Collections.Generic;
55
using System.Diagnostics;
66
using System.Diagnostics.CodeAnalysis;
7+
using System.Linq;
78
using System.Threading.Tasks;
89

910
namespace Egil.RazorComponents.Testing
@@ -16,20 +17,20 @@ namespace Egil.RazorComponents.Testing
1617
// not a good entrypoint for unit tests, because their asynchrony is all about waiting
1718
// for quiescence. We don't want that in tests because we want to assert about all
1819
// possible states, including loading states.
20+
1921
[SuppressMessage("Usage", "BL0006:Do not use RenderTree types", Justification = "<Pending>")]
20-
public class TestRenderingContext : IComponent, ITestRenderingContext
22+
public class ContainerComponent : IComponent
2123
{
22-
private readonly int _componentId;
24+
private readonly TestRenderer _renderer;
2325
private RenderHandle _renderHandle;
2426

25-
internal TestRenderer Renderer { get; }
27+
public int ComponentId { get; private set; }
2628

27-
public TestRenderingContext(TestRenderer renderer)
29+
public ContainerComponent(TestRenderer renderer)
2830
{
2931
if (renderer is null) throw new ArgumentNullException(nameof(renderer));
30-
31-
Renderer = renderer;
32-
_componentId = renderer.AttachTestRootComponent(this);
32+
_renderer = renderer;
33+
ComponentId = _renderer.AttachTestRootComponent(this);
3334
}
3435

3536
public void Attach(RenderHandle renderHandle)
@@ -39,25 +40,17 @@ public void Attach(RenderHandle renderHandle)
3940

4041
public Task SetParametersAsync(ParameterView parameters)
4142
{
42-
throw new NotImplementedException($"{nameof(TestRenderingContext)} shouldn't receive any parameters");
43-
}
44-
45-
public void RenderComponentUnderTest(RenderFragment renderFragment)
46-
{
47-
Renderer.DispatchAndAssertNoSynchronousErrors(() =>
48-
{
49-
_renderHandle.Render(renderFragment);
50-
});
43+
throw new InvalidOperationException($"{nameof(ContainerComponent)} shouldn't receive any parameters");
5144
}
5245

53-
public List<(int Id, IComponent Component)> GetComponents() => GetComponents<IComponent>();
46+
public (int Id, T Component) GetComponent<T>() => GetComponents<T>().First();
5447

55-
public List<(int Id, T Component)> GetComponents<T>()
48+
public IEnumerable<(int Id, T Component)> GetComponents<T>()
5649
{
57-
var ownFrames = Renderer.GetCurrentRenderTreeFrames(_componentId);
50+
var ownFrames = _renderer.GetCurrentRenderTreeFrames(ComponentId);
5851
if (ownFrames.Count == 0)
5952
{
60-
throw new InvalidOperationException($"{nameof(TestRenderingContext)} hasn't yet rendered");
53+
throw new InvalidOperationException($"{nameof(ContainerComponent)} hasn't yet rendered");
6154
}
6255

6356
var result = new List<(int Id, T Component)>();
@@ -74,26 +67,33 @@ public void RenderComponentUnderTest(RenderFragment renderFragment)
7467
return result;
7568
}
7669

77-
public string GetHtml(int componentId)
70+
public void RenderComponentUnderTest(Type componentType, ParameterView parameters)
7871
{
79-
return Htmlizer.GetHtml(Renderer, componentId);
80-
}
72+
_renderer.DispatchAndAssertNoSynchronousErrors(() =>
73+
{
74+
_renderHandle.Render(builder =>
75+
{
76+
builder.OpenComponent(0, componentType);
8177

82-
public void WaitForNextRender(Action trigger)
83-
{
84-
var task = Renderer.NextRender;
85-
if (!(trigger is null)) trigger();
86-
task.Wait(millisecondsTimeout: 1000);
78+
foreach (var parameterValue in parameters)
79+
{
80+
builder.AddAttribute(1, parameterValue.Name, parameterValue.Value);
81+
}
8782

88-
if (!task.IsCompleted)
89-
{
90-
throw new TimeoutException("No render occurred within the timeout period.");
91-
}
83+
builder.CloseComponent();
84+
});
85+
});
9286
}
9387

94-
public void DispatchAndAssertNoSynchronousErrors(Action dispatchAction)
88+
public void RenderComponentUnderTest(RenderFragment renderFragment)
9589
{
96-
Renderer.DispatchAndAssertNoSynchronousErrors(dispatchAction);
90+
_renderer.DispatchAndAssertNoSynchronousErrors(() =>
91+
{
92+
_renderHandle.Render(builder =>
93+
{
94+
builder.AddContent(0, renderFragment);
95+
});
96+
});
9797
}
9898
}
9999
}

src/Components/Fixture.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading.Tasks;
4+
using Microsoft.AspNetCore.Components;
5+
6+
namespace Egil.RazorComponents.Testing
7+
{
8+
public delegate void Test(TestContext context);
9+
10+
public class Fixture : FragmentBase
11+
{
12+
private Test _setup = NoopTestMethod;
13+
private Test _test = NoopTestMethod;
14+
private IReadOnlyCollection<Test> _tests = Array.Empty<Test>();
15+
16+
[Parameter] public Test Setup { get => _setup; set => _setup = value ?? NoopTestMethod; }
17+
[Parameter] public Test Test { get => _test; set => _test = value ?? NoopTestMethod; }
18+
[Parameter] public IReadOnlyCollection<Test> Tests { get => _tests; set => _tests = value ?? Array.Empty<Test>(); }
19+
20+
private static void NoopTestMethod(TestContext context) { }
21+
}
22+
23+
public abstract class FragmentBase : IComponent
24+
{
25+
[Parameter] public RenderFragment ChildContent { get; set; } = default!;
26+
27+
public void Attach(RenderHandle renderHandle) { }
28+
29+
public virtual Task SetParametersAsync(ParameterView parameters)
30+
{
31+
parameters.SetParameterProperties(this);
32+
if (ChildContent is null) throw new InvalidOperationException($"No {nameof(ChildContent)} specified.");
33+
return Task.CompletedTask;
34+
}
35+
}
36+
37+
public class ComponentUnderTest : FragmentBase
38+
{
39+
public override Task SetParametersAsync(ParameterView parameters)
40+
{
41+
var result = base.SetParametersAsync(parameters);
42+
return result;
43+
}
44+
}
45+
46+
public class Fragment : FragmentBase
47+
{
48+
[Parameter] public string Id { get; set; } = string.Empty;
49+
}
50+
}
Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22
using System.Collections.Generic;
33
using System.Linq;
44
using System.Text;
5-
using System.Threading.Tasks;
6-
using AngleSharp.Html.Dom;
7-
using Microsoft.AspNetCore.Components;
5+
using Egil.RazorComponents.Testing.Diffing;
86
using Microsoft.AspNetCore.Components.Rendering;
97
using Microsoft.Extensions.DependencyInjection;
108
using Microsoft.Extensions.Logging;
@@ -17,13 +15,10 @@ public abstract class TestComponentBase : IDisposable
1715
{
1816
private readonly ServiceCollection _serviceCollection = new ServiceCollection();
1917
private readonly Lazy<TestRenderer> _renderer;
20-
private readonly Lazy<IHtmlComparer> _htmlComparer;
2118
private bool _isDisposed = false;
2219

2320
public TestComponentBase()
2421
{
25-
_htmlComparer = new Lazy<IHtmlComparer>(() => new HtmlComparer());
26-
_serviceCollection.AddSingleton(_ => _htmlComparer.Value);
2722
_renderer = new Lazy<TestRenderer>(() =>
2823
{
2924
var serviceProvider = _serviceCollection.BuildServiceProvider();
@@ -35,14 +30,20 @@ public TestComponentBase()
3530
[Fact]
3631
public void ComponentTest()
3732
{
38-
var renderingContext = new TestRenderingContext(_renderer.Value);
39-
renderingContext.RenderComponentUnderTest(BuildRenderTree);
33+
var container = new ContainerComponent(_renderer.Value);
34+
container.RenderComponentUnderTest(BuildRenderTree);
4035

41-
foreach (var (id, component) in renderingContext.GetComponents())
36+
foreach (var (_, fixture) in container.GetComponents<Fixture>())
4237
{
43-
if (component is ITest test)
38+
container.RenderComponentUnderTest(fixture.ChildContent);
39+
var testData = container.GetComponents<FragmentBase>().Select(x => x.Component).ToArray();
40+
41+
using var context = new TestContext(testData);
42+
fixture.Setup(context);
43+
fixture.Test(context);
44+
foreach (var test in fixture.Tests)
4445
{
45-
test.ExecuteTest();
46+
test(context);
4647
}
4748
}
4849
}
@@ -56,7 +57,6 @@ protected virtual void Dispose(bool disposing)
5657
if (disposing)
5758
{
5859
if (_renderer.IsValueCreated) _renderer.Value.Dispose();
59-
if(_htmlComparer.IsValueCreated) _htmlComparer.Value.Dispose();
6060
}
6161
_isDisposed = true;
6262
}

0 commit comments

Comments
 (0)