Skip to content

Commit eefce76

Browse files
committed
Fixed bug with bubbling events and elements being removed during during event handling/rendering
1 parent 71ff116 commit eefce76

16 files changed

Lines changed: 283 additions & 110 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ List of any bug fixes.
2424

2525
- When an `Add` call to the component parameter collection builder was used to select a parameter that was inherited from a base component, the builder incorrectly reported the selected property/parameter as missing on the type. Reported by [@nickmuller](https://github.com/nickmuller) in [#250](https://github.com/egil/bUnit/issues/250).
2626

27+
- When an element, found in the DOM tree using the `Find()`, method was removed because of an event handler trigger on it, e.g. an `cut.Find("button").Click()` event trigger method, an `ElementNotFoundException` was thrown. Reported by [@nickmuller](https://github.com/nickmuller) in [#251](https://github.com/egil/bUnit/issues/251).
28+
2729
## [1.0.0-beta 10] - 2020-09-15
2830

2931
The following section list all changes in beta-10.

src/bunit.core/ComponentParameterCollectionBuilder.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ public bool TryAdd<TValue>(string name, [AllowNull] TValue value)
327327
return true;
328328
}
329329

330-
if(ccProp.GetCustomAttribute<CascadingParameterAttribute>(inherit: false) is CascadingParameterAttribute cpa)
330+
if (ccProp.GetCustomAttribute<CascadingParameterAttribute>(inherit: false) is CascadingParameterAttribute cpa)
331331
{
332332
AddCascadingValueParameter(cpa.Name, value);
333333
return true;
@@ -351,7 +351,7 @@ private static (string paramName, string? cascadingValueName, bool isCascading)
351351
var propertyInfo = propInfoCandidate.DeclaringType != TComponentType
352352
? TComponentType.GetProperty(propInfoCandidate.Name, propInfoCandidate.PropertyType)
353353
: propInfoCandidate;
354-
354+
355355
var paramAttr = propertyInfo?.GetCustomAttribute<ParameterAttribute>(inherit: true);
356356
var cascadingParamAttr = propertyInfo?.GetCustomAttribute<CascadingParameterAttribute>(inherit: true);
357357

src/bunit.web/Diffing/BlazorDiffingHelpers.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,6 @@ public static FilterDecision BlazorAttributeFilter(in AttributeComparisonSource
1919
return FilterDecision.Exclude;
2020

2121
return currentDecision;
22-
}
22+
}
2323
}
2424
}

src/bunit.web/EventDispatchExtensions/GeneralEventDispatchExtensions.cs

Lines changed: 13 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
using System;
2-
using System.Collections.Generic;
3-
using System.Globalization;
42
using System.Threading.Tasks;
53
using AngleSharp.Dom;
6-
using AngleSharp.Html.Dom;
7-
using AngleSharpWrappers;
8-
using Bunit.Rendering;
9-
using Microsoft.AspNetCore.Components.RenderTree;
104

115
namespace Bunit
126
{
@@ -15,73 +9,6 @@ namespace Bunit
159
/// </summary>
1610
public static class GeneralEventDispatchExtensions
1711
{
18-
private static HashSet<string> NonBubblingEvents = new HashSet<string> { "onabort", "onblur", "onchange", "onerror", "onfocus", "onload", "onloadend", "onloadstart", "onmouseenter", "onmouseleave", "onprogress", "onreset", "onscroll", "onsubmit", "onunload", "ontoggle", "ondomnodeinsertedintodocument", "ondomnoderemovedfromdocument" };
19-
private static HashSet<string> DisabledEventNames = new HashSet<string> { "onclick", "ondblclick", "onmousedown", "onmousemove", "onmouseup" };
20-
21-
/// <summary>
22-
/// Raises the event <paramref name="eventName"/> on the element <paramref name="element"/>
23-
/// passing the <paramref name="eventArgs"/> to the event handler.
24-
/// </summary>
25-
/// <param name="element">The element to raise the event on.</param>
26-
/// <param name="eventName">The name of the event to raise (using on-form, e.g. <c>onclick</c>).</param>
27-
/// <param name="eventArgs">The event arguments to pass to the event handler</param>
28-
/// <returns></returns>
29-
public static Task TriggerEventAsync(this IElement element, string eventName, EventArgs eventArgs)
30-
{
31-
if (element is null)
32-
throw new ArgumentNullException(nameof(element));
33-
if (eventName is null)
34-
throw new ArgumentNullException(nameof(eventName));
35-
var renderer = element.Owner.Context.GetService<ITestRenderer>();
36-
if (renderer is null)
37-
throw new InvalidOperationException($"Blazor events can only be raised on elements rendered with the Blazor test renderer '{nameof(ITestRenderer)}'.");
38-
39-
var eventAttrName = Htmlizer.ToBlazorAttribute(eventName);
40-
var eventStopPropergationAttrName = $"{eventAttrName}:stoppropagation";
41-
var result = new List<Task>();
42-
var isNonBubblingEvent = NonBubblingEvents.Contains(eventName.ToLowerInvariant());
43-
44-
foreach (var candidate in element.GetParentsAndSelf())
45-
{
46-
if (candidate.TryGetEventId(eventAttrName, out var id))
47-
result.Add(renderer.DispatchEventAsync(id, new EventFieldInfo() { FieldValue = eventName }, eventArgs));
48-
49-
if (isNonBubblingEvent || candidate.HasAttribute(eventStopPropergationAttrName) || candidate.EventIsDisabled(eventName))
50-
{
51-
break;
52-
}
53-
}
54-
55-
if (result.Count == 0)
56-
throw new MissingEventHandlerException(element, eventName);
57-
58-
return Task.WhenAll(result);
59-
}
60-
61-
private static bool EventIsDisabled(this IElement element, string eventName)
62-
{
63-
// We want to replicate the normal DOM event behavior that, for 'interactive' elements
64-
// with a 'disabled' attribute, certain mouse events are suppressed
65-
66-
var elm = (element as ElementWrapper)?.WrappedElement ?? element;
67-
switch (elm)
68-
{
69-
case IHtmlButtonElement:
70-
case IHtmlInputElement:
71-
case IHtmlTextAreaElement:
72-
case IHtmlSelectElement:
73-
return DisabledEventNames.Contains(eventName) && elm.IsDisabled();
74-
default:
75-
return false;
76-
}
77-
}
78-
79-
private static bool TryGetEventId(this IElement element, string blazorEventName, out ulong id)
80-
{
81-
var eventId = element.GetAttribute(blazorEventName);
82-
return ulong.TryParse(eventId, NumberStyles.Integer, CultureInfo.InvariantCulture, out id);
83-
}
84-
8512
/// <summary>
8613
/// Raises the <c>@onactivate</c> event on <paramref name="element"/>, passing an empty (<see cref="EventArgs.Empty"/>)
8714
/// to the event handler.
@@ -95,7 +22,7 @@ private static bool TryGetEventId(this IElement element, string blazorEventName,
9522
/// </summary>
9623
/// <param name="element">The element to raise the event on.</param>
9724
/// <returns>A task that completes when the event handler is done.</returns>
98-
private static Task ActivateAsync(this IElement element) => TriggerEventAsync(element, "onactivate", EventArgs.Empty);
25+
private static Task ActivateAsync(this IElement element) => element.TriggerEventAsync("onactivate", EventArgs.Empty);
9926

10027
/// <summary>
10128
/// Raises the <c>@onbeforeactivate</c> event on <paramref name="element"/>, passing an empty (<see cref="EventArgs.Empty"/>)
@@ -110,7 +37,7 @@ private static bool TryGetEventId(this IElement element, string blazorEventName,
11037
/// </summary>
11138
/// <param name="element">The element to raise the event on.</param>
11239
/// <returns>A task that completes when the event handler is done.</returns>
113-
private static Task BeforeActivateAsync(this IElement element) => TriggerEventAsync(element, "onbeforeactivate", EventArgs.Empty);
40+
private static Task BeforeActivateAsync(this IElement element) => element.TriggerEventAsync("onbeforeactivate", EventArgs.Empty);
11441

11542
/// <summary>
11643
/// Raises the <c>@onbeforedeactivate</c> event on <paramref name="element"/>, passing an empty (<see cref="EventArgs.Empty"/>)
@@ -125,7 +52,7 @@ private static bool TryGetEventId(this IElement element, string blazorEventName,
12552
/// </summary>
12653
/// <param name="element">The element to raise the event on.</param>
12754
/// <returns>A task that completes when the event handler is done.</returns>
128-
private static Task BeforeDeactivateAsync(this IElement element) => TriggerEventAsync(element, "onbeforedeactivate", EventArgs.Empty);
55+
private static Task BeforeDeactivateAsync(this IElement element) => element.TriggerEventAsync("onbeforedeactivate", EventArgs.Empty);
12956

13057
/// <summary>
13158
/// Raises the <c>@ondeactivate</c> event on <paramref name="element"/>, passing an empty (<see cref="EventArgs.Empty"/>)
@@ -140,7 +67,7 @@ private static bool TryGetEventId(this IElement element, string blazorEventName,
14067
/// </summary>
14168
/// <param name="element">The element to raise the event on.</param>
14269
/// <returns>A task that completes when the event handler is done.</returns>
143-
private static Task DeactivateAsync(this IElement element) => TriggerEventAsync(element, "ondeactivate", EventArgs.Empty);
70+
private static Task DeactivateAsync(this IElement element) => element.TriggerEventAsync("ondeactivate", EventArgs.Empty);
14471

14572
/// <summary>
14673
/// Raises the <c>@onended</c> event on <paramref name="element"/>, passing an empty (<see cref="EventArgs.Empty"/>)
@@ -155,7 +82,7 @@ private static bool TryGetEventId(this IElement element, string blazorEventName,
15582
/// </summary>
15683
/// <param name="element">The element to raise the event on.</param>
15784
/// <returns>A task that completes when the event handler is done.</returns>
158-
private static Task EndedAsync(this IElement element) => TriggerEventAsync(element, "onended", EventArgs.Empty);
85+
private static Task EndedAsync(this IElement element) => element.TriggerEventAsync("onended", EventArgs.Empty);
15986

16087
/// <summary>
16188
/// Raises the <c>@onfullscreenchange</c> event on <paramref name="element"/>, passing an empty (<see cref="EventArgs.Empty"/>)
@@ -170,7 +97,7 @@ private static bool TryGetEventId(this IElement element, string blazorEventName,
17097
/// </summary>
17198
/// <param name="element">The element to raise the event on.</param>
17299
/// <returns>A task that completes when the event handler is done.</returns>
173-
private static Task FullscreenChangeAsync(this IElement element) => TriggerEventAsync(element, "onfullscreenchange", EventArgs.Empty);
100+
private static Task FullscreenChangeAsync(this IElement element) => element.TriggerEventAsync("onfullscreenchange", EventArgs.Empty);
174101

175102
/// <summary>
176103
/// Raises the <c>@onfullscreenerror</c> event on <paramref name="element"/>, passing an empty (<see cref="EventArgs.Empty"/>)
@@ -185,7 +112,7 @@ private static bool TryGetEventId(this IElement element, string blazorEventName,
185112
/// </summary>
186113
/// <param name="element">The element to raise the event on.</param>
187114
/// <returns>A task that completes when the event handler is done.</returns>
188-
private static Task FullscreenErrorAsync(this IElement element) => TriggerEventAsync(element, "onfullscreenerror", EventArgs.Empty);
115+
private static Task FullscreenErrorAsync(this IElement element) => element.TriggerEventAsync("onfullscreenerror", EventArgs.Empty);
189116

190117
/// <summary>
191118
/// Raises the <c>@onloadeddata</c> event on <paramref name="element"/>, passing an empty (<see cref="EventArgs.Empty"/>)
@@ -200,7 +127,7 @@ private static bool TryGetEventId(this IElement element, string blazorEventName,
200127
/// </summary>
201128
/// <param name="element">The element to raise the event on.</param>
202129
/// <returns>A task that completes when the event handler is done.</returns>
203-
private static Task LoadedDataAsync(this IElement element) => TriggerEventAsync(element, "onloadeddata", EventArgs.Empty);
130+
private static Task LoadedDataAsync(this IElement element) => element.TriggerEventAsync("onloadeddata", EventArgs.Empty);
204131

205132
/// <summary>
206133
/// Raises the <c>@onloadedmetadata</c> event on <paramref name="element"/>, passing an empty (<see cref="EventArgs.Empty"/>)
@@ -215,7 +142,7 @@ private static bool TryGetEventId(this IElement element, string blazorEventName,
215142
/// </summary>
216143
/// <param name="element">The element to raise the event on.</param>
217144
/// <returns>A task that completes when the event handler is done.</returns>
218-
private static Task LoadedMetadataAsync(this IElement element) => TriggerEventAsync(element, "onloadedmetadata", EventArgs.Empty);
145+
private static Task LoadedMetadataAsync(this IElement element) => element.TriggerEventAsync("onloadedmetadata", EventArgs.Empty);
219146

220147
/// <summary>
221148
/// Raises the <c>@onpointerlockchange</c> event on <paramref name="element"/>, passing an empty (<see cref="EventArgs.Empty"/>)
@@ -230,7 +157,7 @@ private static bool TryGetEventId(this IElement element, string blazorEventName,
230157
/// </summary>
231158
/// <param name="element">The element to raise the event on.</param>
232159
/// <returns>A task that completes when the event handler is done.</returns>
233-
private static Task PointerlockChangeAsync(this IElement element) => TriggerEventAsync(element, "onpointerlockchange", EventArgs.Empty);
160+
private static Task PointerlockChangeAsync(this IElement element) => element.TriggerEventAsync("onpointerlockchange", EventArgs.Empty);
234161

235162
/// <summary>
236163
/// Raises the <c>@onpointerlockerror</c> event on <paramref name="element"/>, passing an empty (<see cref="EventArgs.Empty"/>)
@@ -245,7 +172,7 @@ private static bool TryGetEventId(this IElement element, string blazorEventName,
245172
/// </summary>
246173
/// <param name="element">The element to raise the event on.</param>
247174
/// <returns>A task that completes when the event handler is done.</returns>
248-
private static Task PointerlockErrorAsync(this IElement element) => TriggerEventAsync(element, "onpointerlockerror", EventArgs.Empty);
175+
private static Task PointerlockErrorAsync(this IElement element) => element.TriggerEventAsync("onpointerlockerror", EventArgs.Empty);
249176

250177
/// <summary>
251178
/// Raises the <c>@onreadystatechange</c> event on <paramref name="element"/>, passing an empty (<see cref="EventArgs.Empty"/>)
@@ -260,7 +187,7 @@ private static bool TryGetEventId(this IElement element, string blazorEventName,
260187
/// </summary>
261188
/// <param name="element">The element to raise the event on.</param>
262189
/// <returns>A task that completes when the event handler is done.</returns>
263-
private static Task ReadystateChangeAsync(this IElement element) => TriggerEventAsync(element, "onreadystatechange", EventArgs.Empty);
190+
private static Task ReadystateChangeAsync(this IElement element) => element.TriggerEventAsync("onreadystatechange", EventArgs.Empty);
264191

265192
/// <summary>
266193
/// Raises the <c>@onscroll</c> event on <paramref name="element"/>, passing an empty (<see cref="EventArgs.Empty"/>)
@@ -275,6 +202,6 @@ private static bool TryGetEventId(this IElement element, string blazorEventName,
275202
/// </summary>
276203
/// <param name="element">The element to raise the event on.</param>
277204
/// <returns>A task that completes when the event handler is done.</returns>
278-
private static Task ScrollAsync(this IElement element) => TriggerEventAsync(element, "onscroll", EventArgs.Empty);
205+
private static Task ScrollAsync(this IElement element) => element.TriggerEventAsync("onscroll", EventArgs.Empty);
279206
}
280207
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Globalization;
4+
using System.Linq;
5+
using System.Threading.Tasks;
6+
using AngleSharp.Dom;
7+
using AngleSharp.Html.Dom;
8+
using AngleSharpWrappers;
9+
using Bunit.Rendering;
10+
using Microsoft.AspNetCore.Components.RenderTree;
11+
12+
namespace Bunit
13+
{
14+
/// <summary>
15+
/// General event dispatch helper extension methods.
16+
/// </summary>
17+
public static class TriggerEventDispatchExtensions
18+
{
19+
private static HashSet<string> NonBubblingEvents = new HashSet<string> { "onabort", "onblur", "onchange", "onerror", "onfocus", "onload", "onloadend", "onloadstart", "onmouseenter", "onmouseleave", "onprogress", "onreset", "onscroll", "onsubmit", "onunload", "ontoggle", "ondomnodeinsertedintodocument", "ondomnoderemovedfromdocument" };
20+
private static HashSet<string> DisabledEventNames = new HashSet<string> { "onclick", "ondblclick", "onmousedown", "onmousemove", "onmouseup" };
21+
22+
/// <summary>
23+
/// Raises the event <paramref name="eventName"/> on the element <paramref name="element"/>
24+
/// passing the <paramref name="eventArgs"/> to the event handler.
25+
/// </summary>
26+
/// <param name="element">The element to raise the event on.</param>
27+
/// <param name="eventName">The name of the event to raise (using on-form, e.g. <c>onclick</c>).</param>
28+
/// <param name="eventArgs">The event arguments to pass to the event handler</param>
29+
/// <returns></returns>
30+
public static Task TriggerEventAsync(this IElement element, string eventName, EventArgs eventArgs)
31+
{
32+
if (element is null)
33+
throw new ArgumentNullException(nameof(element));
34+
if (eventName is null)
35+
throw new ArgumentNullException(nameof(eventName));
36+
var renderer = element.Owner.Context.GetService<ITestRenderer>();
37+
if (renderer is null)
38+
throw new InvalidOperationException($"Blazor events can only be raised on elements rendered with the Blazor test renderer '{nameof(ITestRenderer)}'.");
39+
40+
var isNonBubblingEvent = NonBubblingEvents.Contains(eventName.ToLowerInvariant());
41+
42+
if (isNonBubblingEvent)
43+
return TriggerNonBubblingEventAsync(renderer, element.Unwrap(), eventName, eventArgs);
44+
else
45+
return TriggerBubblingEventAsync(renderer, element.Unwrap(), eventName, eventArgs);
46+
}
47+
48+
private static Task TriggerBubblingEventAsync(ITestRenderer renderer, IElement element, string eventName, EventArgs eventArgs)
49+
{
50+
var eventAttrName = Htmlizer.ToBlazorAttribute(eventName);
51+
var eventStopPropergationAttrName = $"{eventAttrName}:stoppropagation";
52+
var eventIds = new List<ulong>();
53+
54+
foreach (var candidate in element.GetParentsAndSelf())
55+
{
56+
if (candidate.TryGetEventId(eventAttrName, out var id))
57+
eventIds.Add(id);
58+
59+
if (candidate.HasAttribute(eventStopPropergationAttrName) || candidate.EventIsDisabled(eventName))
60+
{
61+
break;
62+
}
63+
}
64+
65+
if (eventIds.Count == 0)
66+
throw new MissingEventHandlerException(element, eventName);
67+
68+
return Task.WhenAll(eventIds.Select(TriggerEvent).ToArray());
69+
70+
Task TriggerEvent(ulong id)
71+
=> renderer.DispatchEventAsync(id, new EventFieldInfo() { FieldValue = eventName }, eventArgs);
72+
}
73+
74+
private static Task TriggerNonBubblingEventAsync(ITestRenderer renderer, IElement element, string eventName, EventArgs eventArgs)
75+
{
76+
var eventAttrName = Htmlizer.ToBlazorAttribute(eventName);
77+
78+
if (element.TryGetEventId(eventAttrName, out var id))
79+
return renderer.DispatchEventAsync(id, new EventFieldInfo() { FieldValue = eventName }, eventArgs);
80+
else
81+
throw new MissingEventHandlerException(element, eventName);
82+
}
83+
84+
private static bool EventIsDisabled(this IElement element, string eventName)
85+
{
86+
// We want to replicate the normal DOM event behavior that, for 'interactive' elements
87+
// with a 'disabled' attribute, certain mouse events are suppressed
88+
89+
switch (element)
90+
{
91+
case IHtmlButtonElement:
92+
case IHtmlInputElement:
93+
case IHtmlTextAreaElement:
94+
case IHtmlSelectElement:
95+
return DisabledEventNames.Contains(eventName) && element.IsDisabled();
96+
default:
97+
return false;
98+
}
99+
}
100+
101+
private static bool TryGetEventId(this IElement element, string blazorEventName, out ulong id)
102+
{
103+
var eventId = element.GetAttribute(blazorEventName);
104+
return ulong.TryParse(eventId, NumberStyles.Integer, CultureInfo.InvariantCulture, out id);
105+
}
106+
}
107+
}

src/bunit.web/Extensions/ElementNotFoundException.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,15 @@ public class ElementNotFoundException : Exception
1414
public string CssSelector { get; }
1515

1616
/// <inheritdoc/>
17-
public ElementNotFoundException(string cssSelector) : base($"No elements were found that matches the selector '{cssSelector}'")
17+
public ElementNotFoundException(string cssSelector)
18+
: base($"No elements were found that matches the selector '{cssSelector}'")
19+
{
20+
CssSelector = cssSelector;
21+
}
22+
23+
/// <inheritdoc/>
24+
protected ElementNotFoundException(string message, string cssSelector)
25+
: base(message)
1826
{
1927
CssSelector = cssSelector;
2028
}

src/bunit.web/Extensions/ElementRemovedException.cs

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

0 commit comments

Comments
 (0)