-
-
Notifications
You must be signed in to change notification settings - Fork 112
/
Copy pathRenderedComponent.cs
194 lines (168 loc) · 5.09 KB
/
RenderedComponent.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
using System.Diagnostics;
using AngleSharp.Dom;
using Bunit.Rendering;
namespace Bunit;
/// <summary>
/// Represents a rendered component.
/// </summary>
[DebuggerDisplay("Component={typeof(TComponent).Name,nq},RenderCount={RenderCount}")]
internal sealed class RenderedComponent<TComponent> : ComponentState, IRenderedComponent<TComponent>, IRenderedComponent
where TComponent : IComponent
{
private readonly BunitRenderer renderer;
private readonly TComponent instance;
[SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "Owned by BunitServiceProvider, disposed by it.")]
private readonly BunitHtmlParser htmlParser;
private int renderCount;
private string markup = string.Empty;
private int markupStartIndex;
private int markupEndIndex;
private INodeList? latestRenderNodes;
public bool IsDirty { get; set; }
/// <summary>
/// Gets the component under test.
/// </summary>
public TComponent Instance
{
get
{
EnsureComponentNotDisposed();
return instance ?? throw new InvalidOperationException("Component has not rendered yet...");
}
}
/// <summary>
/// Gets a value indicating whether the rendered component or fragment has been disposed by the <see cref="BunitRenderer"/>.
/// </summary>
public bool IsDisposed { get; private set; }
/// <summary>
/// Gets the HTML markup from the rendered fragment/component.
/// </summary>
public string Markup
{
get
{
EnsureComponentNotDisposed();
// Volatile read is necessary to ensure the updated markup
// is available across CPU cores. Without it, the pointer to the
// markup string can be stored in a CPUs register and not
// get updated when another CPU changes the string.
return Volatile.Read(ref markup);
}
}
/// <summary>
/// Adds or removes an event handler that will be triggered after
/// each render of this <see cref="RenderedComponent{T}"/>.
/// </summary>
public event EventHandler? OnAfterRender;
/// <summary>
/// An event that is raised after the markup of the
/// <see cref="RenderedComponent{T}"/> is updated.
/// </summary>
public event EventHandler? OnMarkupUpdated;
/// <summary>
/// Gets the total number times the fragment has been through its render life-cycle.
/// </summary>
public int RenderCount => renderCount;
/// <summary>
/// Gets the AngleSharp <see cref="INodeList"/> based
/// on the HTML markup from the rendered fragment/component.
/// </summary>
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
public INodeList Nodes
{
get
{
EnsureComponentNotDisposed();
return latestRenderNodes ??= htmlParser.Parse(Markup);
}
}
/// <summary>
/// Gets the <see cref="IServiceProvider"/> used when rendering the component.
/// </summary>
public IServiceProvider Services { get; }
int IRenderedComponent.RenderCount { get => renderCount; set { renderCount = value; } }
public IRenderedComponent? Root { get; }
public RenderedComponent(
BunitRenderer renderer,
int componentId,
IComponent instance,
IServiceProvider services,
ComponentState? parentComponentState)
: base(renderer, componentId, instance, parentComponentState)
{
Services = services;
this.renderer = renderer;
this.instance = (TComponent)instance;
htmlParser = Services.GetRequiredService<BunitHtmlParser>();
var parentRenderedComponent = parentComponentState as IRenderedComponent;
Root = parentRenderedComponent?.Root ?? parentRenderedComponent;
}
/// <inheritdoc/>
public void Dispose()
{
if (IsDisposed)
return;
if (Root is not null)
Root.IsDirty = true;
IsDisposed = true;
markup = string.Empty;
OnAfterRender = null;
OnMarkupUpdated = null;
}
/// <inheritdoc/>
public override ValueTask DisposeAsync()
{
Dispose();
return base.DisposeAsync();
}
public void SetMarkupIndices(int start, int end)
{
markupStartIndex = start;
markupEndIndex = end;
IsDirty = true;
}
/// <summary>
/// Called by the owning <see cref="BunitRenderer"/> when it finishes a render.
/// </summary>
public void UpdateMarkup()
{
if (IsDisposed)
return;
if (Root is RenderedComponent<BunitRootComponent> root)
{
var newMarkup = root.markup[markupStartIndex..markupEndIndex];
if (markup != newMarkup)
{
Volatile.Write(ref markup, newMarkup);
latestRenderNodes = null;
OnMarkupUpdated?.Invoke(this, EventArgs.Empty);
}
else
{
// no change
}
}
else
{
var newMarkup = Htmlizer.GetHtml(ComponentId, renderer);
// Volatile write is necessary to ensure the updated markup
// is available across CPU cores. Without it, the pointer to the
// markup string can be stored in a CPUs register and not
// get updated when another CPU changes the string.
Volatile.Write(ref markup, newMarkup);
latestRenderNodes = null;
OnMarkupUpdated?.Invoke(this, EventArgs.Empty);
}
IsDirty = false;
OnAfterRender?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Ensures that the underlying component behind the
/// fragment has not been removed from the render tree.
/// </summary>
private void EnsureComponentNotDisposed()
{
if (IsDisposed)
throw new ComponentDisposedException(ComponentId);
}
}