Skip to content

Commit 7f4b21d

Browse files
committed
Updates to preview-9
1 parent b9c3fad commit 7f4b21d

11 files changed

+306
-10
lines changed

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<Project>
22
<PropertyGroup>
3-
<AspNetCoreVersion>3.0.0-preview8.19405.7</AspNetCoreVersion>
3+
<AspNetCoreVersion>3.0.0-preview9.19424.4</AspNetCoreVersion>
44
</PropertyGroup>
55
</Project>

sample/RazorComponentLibTests/AlertTests.razor

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ and re-render, use the component reference syntax and the Render method.
9090
BUG: Currently @@ref doesn't actually generate the required backing field: https://github.com/aspnet/AspNetCore/issues/13099
9191

9292
@code {
93+
Alert sut;
9394
bool isVisible = true;
9495

9596
[Fact(DisplayName = "When Visible is toggled to false, all child content is removed from alert")]
@@ -98,16 +99,16 @@ BUG: Currently @@ref doesn't actually generate the required backing field: https
9899
// initial assert
99100
var result = RenderResults.Single(x => x.Id == (nameof(DismissTest)));
100101
result.RenderedHtml.ShouldBe(result.Snippets[0]);
101-
//Assert.Equal(true, sut.Visible); BUG: https://github.com/aspnet/AspNetCore/issues/13099
102+
Assert.Equal(true, sut.Visible);
102103

103104
// act
104105
isVisible = false;
105106
this.Render();
106107

107-
// dismiss assert
108+
// assert
108109
var dismissResult = RenderResults.Single(x => x.Id == (nameof(DismissTest)));
109110
dismissResult.RenderedHtml.ShouldBe(result.Snippets[1]);
110-
//Assert.Equal(false, sut.Visible); BUG: https://github.com/aspnet/AspNetCore/issues/13099
111+
Assert.Equal(false, sut.Visible);
111112
}
112113
}
113114
<Fact Id=@nameof(DismissTest)>

sample/RazorComponentLibTests/RazorComponentLibTests.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
<PackageReference Include="Microsoft.AspNetCore.Components" Version="$(AspNetCoreVersion)" />
1414
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="$(AspNetCoreVersion)" />
1515
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
16-
<PackageReference Include="Moq" Version="4.12.0" />
17-
<PackageReference Include="Razor.Components.Testing.Library" Version="0.1.0-preview8-19405-7-1" />
16+
<PackageReference Include="Moq" Version="4.13.0" />
17+
<PackageReference Include="Razor.Components.Testing.Library" Version="0.1.0-preview9-19424-4-1" />
1818
<PackageReference Include="xunit.assert" Version="2.4.1" />
1919
<PackageReference Include="xunit.core" Version="2.4.1" />
2020
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">

src/Egil.RazorComponents.Testing.Library.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<RepositoryUrl>https://github.com/egil/razor-components-testing-library</RepositoryUrl>
1414
<PackageProjectUrl>https://github.com/egil/razor-components-testing-library</PackageProjectUrl>
1515
<PackageTags>razor-components unit-testing testing blazor blazor-server-side blazor-client-side</PackageTags>
16-
<Version>0.1.0-preview8-19405-7-1</Version>
16+
<Version>0.1.0-preview9-19424-4-1</Version>
1717
<Authors>Egil Hansen</Authors>
1818
<Company>Egil Hansen</Company>
1919
<Product>Razor Component Testing Library</Product>

src/Fact.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using Microsoft.AspNetCore.Components;
3+
using Microsoft.AspNetCore.Components.Rendering;
34
using Microsoft.AspNetCore.Components.RenderTree;
45

56
namespace Egil.RazorComponents.Testing

src/GlobalSuppressions.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "BL0006:The types in 'Microsoft.AspNetCore.Components.RenderTree' are not recommended for use outside of the Blazor framework. These type definitions will change in future releases.",
2+
Justification = "I will take the chance",
3+
Scope = "namespaceanddescendants",
4+
Target = "Egil.RazorComponents.Testing")]

src/RazorComponentTest.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System;
22
using System.Collections.Generic;
3+
using Egil.RazorComponents.Testing.Rendering;
4+
using Microsoft.AspNetCore.Components.Rendering;
35
using Microsoft.AspNetCore.Components.RenderTree;
46
using Microsoft.Extensions.DependencyInjection;
57
using Xunit;

src/RenderFragmentWrapper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using System;
22
using Microsoft.AspNetCore.Components;
3-
using Microsoft.AspNetCore.Components.RenderTree;
3+
using Microsoft.AspNetCore.Components.Rendering;
44

55
namespace Egil.RazorComponents.Testing
66
{
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System.Collections.Generic;
2+
3+
namespace Egil.RazorComponents.Testing.Rendering
4+
{
5+
public readonly struct ComponentRenderedText
6+
{
7+
public ComponentRenderedText(int componentId, IEnumerable<string> tokens)
8+
{
9+
ComponentId = componentId;
10+
Tokens = tokens;
11+
}
12+
13+
public int ComponentId { get; }
14+
15+
public IEnumerable<string> Tokens { get; }
16+
}
17+
}

src/Rendering/HtmlRenderer.cs

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.Runtime.ExceptionServices;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Microsoft.AspNetCore.Components.RenderTree;
8+
using Microsoft.Extensions.Logging;
9+
using Microsoft.AspNetCore.Components;
10+
11+
namespace Egil.RazorComponents.Testing.Rendering
12+
{
13+
/// <summary>
14+
/// Copied from <see cref="https://github.com/aspnet/AspNetCore/blob/v3.0.0-preview9.19424.4/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/HtmlRenderer.cs"/>
15+
/// since this class was made internal in preview-9
16+
/// </summary>
17+
public class HtmlRenderer : Renderer
18+
{
19+
private static readonly HashSet<string> SelfClosingElements = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
20+
{
21+
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"
22+
};
23+
24+
private static readonly Task CanceledRenderTask = Task.FromCanceled(new CancellationToken(canceled: true));
25+
26+
private readonly Func<string, string> _htmlEncoder;
27+
28+
public HtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, Func<string, string> htmlEncoder)
29+
: base(serviceProvider, loggerFactory)
30+
{
31+
_htmlEncoder = htmlEncoder;
32+
}
33+
34+
public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault();
35+
36+
/// <inheritdoc />
37+
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
38+
{
39+
// By default we return a canceled task. This has the effect of making it so that the
40+
// OnAfterRenderAsync callbacks on components don't run by default.
41+
// This way, by default prerendering gets the correct behavior and other renderers
42+
// override the UpdateDisplayAsync method already, so those components can
43+
// either complete a task when the client acknowledges the render, or return a canceled task
44+
// when the renderer gets disposed.
45+
46+
// We believe that returning a canceled task is the right behavior as we expect that any class
47+
// that subclasses this class to provide an implementation for a given rendering scenario respects
48+
// the contract that OnAfterRender should only be called when the display has successfully been updated
49+
// and the application is interactive. (Element and component references are populated and JavaScript interop
50+
// is available).
51+
return CanceledRenderTask;
52+
}
53+
54+
public async Task<ComponentRenderedText> RenderComponentAsync(Type componentType, ParameterView initialParameters)
55+
{
56+
var (componentId, frames) = await CreateInitialRenderAsync(componentType, initialParameters);
57+
58+
var context = new HtmlRenderingContext();
59+
var newPosition = RenderFrames(context, frames, 0, frames.Count);
60+
Debug.Assert(newPosition == frames.Count);
61+
return new ComponentRenderedText(componentId, context.Result);
62+
}
63+
64+
public Task<ComponentRenderedText> RenderComponentAsync<TComponent>(ParameterView initialParameters) where TComponent : IComponent
65+
{
66+
return RenderComponentAsync(typeof(TComponent), initialParameters);
67+
}
68+
69+
/// <inheritdoc />
70+
protected override void HandleException(Exception exception)
71+
=> ExceptionDispatchInfo.Capture(exception).Throw();
72+
73+
private int RenderFrames(HtmlRenderingContext context, ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
74+
{
75+
var nextPosition = position;
76+
var endPosition = position + maxElements;
77+
while (position < endPosition)
78+
{
79+
nextPosition = RenderCore(context, frames, position);
80+
if (position == nextPosition)
81+
{
82+
throw new InvalidOperationException("We didn't consume any input.");
83+
}
84+
position = nextPosition;
85+
}
86+
87+
return nextPosition;
88+
}
89+
90+
private int RenderCore(
91+
HtmlRenderingContext context,
92+
ArrayRange<RenderTreeFrame> frames,
93+
int position)
94+
{
95+
ref var frame = ref frames.Array[position];
96+
switch (frame.FrameType)
97+
{
98+
case RenderTreeFrameType.Element:
99+
return RenderElement(context, frames, position);
100+
case RenderTreeFrameType.Attribute:
101+
throw new InvalidOperationException($"Attributes should only be encountered within {nameof(RenderElement)}");
102+
case RenderTreeFrameType.Text:
103+
context.Result.Add(_htmlEncoder(frame.TextContent));
104+
return ++position;
105+
case RenderTreeFrameType.Markup:
106+
context.Result.Add(frame.MarkupContent);
107+
return ++position;
108+
case RenderTreeFrameType.Component:
109+
return RenderChildComponent(context, frames, position);
110+
case RenderTreeFrameType.Region:
111+
return RenderFrames(context, frames, position + 1, frame.RegionSubtreeLength - 1);
112+
case RenderTreeFrameType.ElementReferenceCapture:
113+
case RenderTreeFrameType.ComponentReferenceCapture:
114+
return ++position;
115+
default:
116+
throw new InvalidOperationException($"Invalid element frame type '{frame.FrameType}'.");
117+
}
118+
}
119+
120+
private int RenderChildComponent(
121+
HtmlRenderingContext context,
122+
ArrayRange<RenderTreeFrame> frames,
123+
int position)
124+
{
125+
ref var frame = ref frames.Array[position];
126+
var childFrames = GetCurrentRenderTreeFrames(frame.ComponentId);
127+
RenderFrames(context, childFrames, 0, childFrames.Count);
128+
return position + frame.ComponentSubtreeLength;
129+
}
130+
131+
private int RenderElement(
132+
HtmlRenderingContext context,
133+
ArrayRange<RenderTreeFrame> frames,
134+
int position)
135+
{
136+
ref var frame = ref frames.Array[position];
137+
var result = context.Result;
138+
result.Add("<");
139+
result.Add(frame.ElementName);
140+
var afterAttributes = RenderAttributes(context, frames, position + 1, frame.ElementSubtreeLength - 1, out var capturedValueAttribute);
141+
142+
// When we see an <option> as a descendant of a <select>, and the option's "value" attribute matches the
143+
// "value" attribute on the <select>, then we auto-add the "selected" attribute to that option. This is
144+
// a way of converting Blazor's select binding feature to regular static HTML.
145+
if (context.ClosestSelectValueAsString != null
146+
&& string.Equals(frame.ElementName, "option", StringComparison.OrdinalIgnoreCase)
147+
&& string.Equals(capturedValueAttribute, context.ClosestSelectValueAsString, StringComparison.Ordinal))
148+
{
149+
result.Add(" selected");
150+
}
151+
152+
var remainingElements = frame.ElementSubtreeLength + position - afterAttributes;
153+
if (remainingElements > 0)
154+
{
155+
result.Add(">");
156+
157+
var isSelect = string.Equals(frame.ElementName, "select", StringComparison.OrdinalIgnoreCase);
158+
if (isSelect)
159+
{
160+
context.ClosestSelectValueAsString = capturedValueAttribute;
161+
}
162+
163+
var afterElement = RenderChildren(context, frames, afterAttributes, remainingElements);
164+
165+
if (isSelect)
166+
{
167+
// There's no concept of nested <select> elements, so as soon as we're exiting one of them,
168+
// we can safely say there is no longer any value for this
169+
context.ClosestSelectValueAsString = null;
170+
}
171+
172+
result.Add("</");
173+
result.Add(frame.ElementName);
174+
result.Add(">");
175+
Debug.Assert(afterElement == position + frame.ElementSubtreeLength);
176+
return afterElement;
177+
}
178+
else
179+
{
180+
if (SelfClosingElements.Contains(frame.ElementName))
181+
{
182+
result.Add(" />");
183+
}
184+
else
185+
{
186+
result.Add(">");
187+
result.Add("</");
188+
result.Add(frame.ElementName);
189+
result.Add(">");
190+
}
191+
Debug.Assert(afterAttributes == position + frame.ElementSubtreeLength);
192+
return afterAttributes;
193+
}
194+
}
195+
196+
private int RenderChildren(HtmlRenderingContext context, ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
197+
{
198+
if (maxElements == 0)
199+
{
200+
return position;
201+
}
202+
203+
return RenderFrames(context, frames, position, maxElements);
204+
}
205+
206+
private int RenderAttributes(
207+
HtmlRenderingContext context,
208+
ArrayRange<RenderTreeFrame> frames, int position, int maxElements, out string? capturedValueAttribute)
209+
{
210+
capturedValueAttribute = null;
211+
212+
if (maxElements == 0)
213+
{
214+
return position;
215+
}
216+
217+
var result = context.Result;
218+
219+
for (var i = 0; i < maxElements; i++)
220+
{
221+
var candidateIndex = position + i;
222+
ref var frame = ref frames.Array[candidateIndex];
223+
if (frame.FrameType != RenderTreeFrameType.Attribute)
224+
{
225+
return candidateIndex;
226+
}
227+
228+
if (frame.AttributeName.Equals("value", StringComparison.OrdinalIgnoreCase))
229+
{
230+
capturedValueAttribute = frame.AttributeValue as string;
231+
}
232+
233+
switch (frame.AttributeValue)
234+
{
235+
case bool flag when flag:
236+
result.Add(" ");
237+
result.Add(frame.AttributeName);
238+
break;
239+
case string value:
240+
result.Add(" ");
241+
result.Add(frame.AttributeName);
242+
result.Add("=");
243+
result.Add("\"");
244+
result.Add(_htmlEncoder(value));
245+
result.Add("\"");
246+
break;
247+
default:
248+
break;
249+
}
250+
}
251+
252+
return position + maxElements;
253+
}
254+
255+
private async Task<(int, ArrayRange<RenderTreeFrame>)> CreateInitialRenderAsync(Type componentType, ParameterView initialParameters)
256+
{
257+
var component = InstantiateComponent(componentType);
258+
var componentId = AssignRootComponentId(component);
259+
260+
await RenderRootComponentAsync(componentId, initialParameters);
261+
262+
return (componentId, GetCurrentRenderTreeFrames(componentId));
263+
}
264+
265+
private class HtmlRenderingContext
266+
{
267+
public List<string> Result { get; } = new List<string>();
268+
269+
public string? ClosestSelectValueAsString { get; set; }
270+
}
271+
}
272+
}

0 commit comments

Comments
 (0)