Skip to content

Commit fa2024f

Browse files
committed
Triggered events bubble correctly up the DOM/render tree and triggers event handlers higher up, if conditions are met
1 parent 1349958 commit fa2024f

20 files changed

Lines changed: 257 additions & 32 deletions

CHANGELOG.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
All notable changes to **bUnit** will be documented in this file. The project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
44

5+
<!-- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) -->
6+
57
## [UNRELEASED BETA 11]
68

7-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
9+
The following section list all changes in beta-11.
810

911
### Added
1012
List of new features.
@@ -42,17 +44,17 @@ List of soon-to-be removed features.
4244
### Removed
4345
List of now removed features.
4446

47+
- The async event dispatcher helper methods have been removed (e.g. `ClickAsync()`), as they do not provide any benefit. If you have an event that triggers async operations in the component under test, instead use `cut.WaitForState()` or `cut.WaitForAssertion()` to await the expected state in the component.
48+
4549
### Fixed
4650
List of any bug fixes.
4751

4852
- Using the ComponentParameterCollectionBuilder's `Add(p => p.Param, value)` method to add a unnamed cascading value didn't create an unnnamed cascading value parameter. By [@egil](https://github.com/egil) in [#203](https://github.com/egil/bUnit/pull/203). Credits to [Ben Sampica (@benjaminsampica)](https://github.com/benjaminsampica) for reporting and helping investigate this issue.
49-
50-
### Security
51-
List of fixed security vulnerabilities.
53+
- Triggered events now bubble correctly up the DOM tree and triggers other events of the same type. This is a **potentially breaking change,** since this changes the behaviour of event triggering and thus you might see tests start breaking as a result hereof. By [@egil](https://github.com/egil) in [#119](https://github.com/egil/bUnit/issues/119).
5254

5355
## [1.0.0-beta 10] - 2020-09-15
5456

55-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
57+
The following section list all changes in beta-10.
5658

5759
### Added
5860
List of new features.

src/bunit.web/EventDispatchExtensions/GeneralEventDispatchExtensions.cs

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Globalization;
34
using System.Threading.Tasks;
45
using AngleSharp.Dom;
6+
using AngleSharp.Html.Dom;
7+
using AngleSharpWrappers;
58
using Bunit.Rendering;
69
using Microsoft.AspNetCore.Components.RenderTree;
710

@@ -12,6 +15,9 @@ namespace Bunit
1215
/// </summary>
1316
public static class GeneralEventDispatchExtensions
1417
{
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+
1521
/// <summary>
1622
/// Raises the event <paramref name="eventName"/> on the element <paramref name="element"/>
1723
/// passing the <paramref name="eventArgs"/> to the event handler.
@@ -24,20 +30,56 @@ public static Task TriggerEventAsync(this IElement element, string eventName, Ev
2430
{
2531
if (element is null)
2632
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)}'.");
2738

28-
var blazorEventAttr = Htmlizer.ToBlazorAttribute(eventName);
29-
var eventHandlerIdString = element.GetAttribute(blazorEventAttr);
39+
var eventAttrName = Htmlizer.ToBlazorAttribute(eventName);
40+
var eventStopPropergationAttrName = $"{eventAttrName}:stoppropagation";
41+
var result = new List<Task>();
42+
var isNonBubblingEvent = NonBubblingEvents.Contains(eventName.ToLowerInvariant());
3043

31-
if (string.IsNullOrEmpty(eventHandlerIdString))
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)
3256
throw new MissingEventHandlerException(element, eventName);
3357

34-
var eventHandlerId = ulong.Parse(eventHandlerIdString, CultureInfo.InvariantCulture);
58+
return Task.WhenAll(result);
59+
}
3560

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)}'.");
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+
}
3978

40-
return renderer.DispatchEventAsync(eventHandlerId, new EventFieldInfo() { FieldValue = eventName }, eventArgs);
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);
4183
}
4284

4385
/// <summary>

src/bunit.web/Extensions/Internal/AngleSharpExtensions.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Collections.Generic;
2+
using AngleSharp.Diffing.Extensions;
23
using AngleSharp.Dom;
34
using Bunit.Diffing;
45
using Bunit.Rendering;
@@ -63,5 +64,19 @@ public static IEnumerable<INode> AsEnumerable(this INode node)
6364
{
6465
return nodes?.Length > 0 ? nodes[0].GetHtmlComparer() : null;
6566
}
67+
68+
/// <summary>
69+
/// Gets the parents of the <paramref name="element"/>, starting with
70+
/// the <paramref name="element"/> itself.
71+
/// </summary>
72+
public static IEnumerable<IElement> GetParentsAndSelf(this IElement element)
73+
{
74+
yield return element;
75+
foreach (var node in element.GetParents())
76+
{
77+
if (node is IElement parent)
78+
yield return parent;
79+
}
80+
}
6681
}
6782
}

src/bunit.web/Extensions/RefreshableElementCollection.cs renamed to src/bunit.web/Extensions/Internal/RefreshableElementCollection.cs

File renamed without changes.

src/bunit.web/Rendering/Internal/Htmlizer.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ internal static class Htmlizer
2424
private const string BLAZOR_INTERNAL_ATTR_PREFIX = "__internal_";
2525
private const string BLAZOR_CSS_SCOPE_ATTR_PREFIX = "b-";
2626
internal const string BLAZOR_ATTR_PREFIX = "blazor:";
27-
internal const string ELEMENT_REFERENCE_ATTR_NAME = BLAZOR_ATTR_PREFIX + "elementreference";
27+
internal const string ELEMENT_REFERENCE_ATTR_NAME = BLAZOR_ATTR_PREFIX + "elementReference";
2828

2929
public static bool IsBlazorAttribute(string attributeName)
3030
{
@@ -236,10 +236,13 @@ private static int RenderAttributes(
236236
case bool flag when flag && frame.AttributeName.StartsWith(BLAZOR_INTERNAL_ATTR_PREFIX, StringComparison.Ordinal):
237237
// NOTE: This was added to make it more obvious
238238
// that this is a generated/special blazor attribute
239-
// for internal usage
239+
// for internal usage
240+
var nameParts = frame.AttributeName.Split('_', StringSplitOptions.RemoveEmptyEntries);
240241
result.Add(" ");
241242
result.Add(BLAZOR_ATTR_PREFIX);
242-
result.Add(frame.AttributeName);
243+
result.Add(nameParts[2]);
244+
result.Add(":");
245+
result.Add(nameParts[1]);
243246
break;
244247
case bool flag when flag:
245248
result.Add(" ");
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System;
2+
using Microsoft.AspNetCore.Components;
3+
using Microsoft.AspNetCore.Components.Rendering;
4+
using Microsoft.AspNetCore.Components.Web;
5+
6+
namespace Bunit.TestAssets.SampleComponents
7+
{
8+
public class EventBubbles : ComponentBase
9+
{
10+
[Parameter] public string ChildElementType { get; set; } = "div";
11+
[Parameter] public bool ChildElementDisabled { get; set; }
12+
[Parameter] public string? EventName { get; set; }
13+
[Parameter] public bool GrandParentStopPropergation { get; set; }
14+
[Parameter] public bool ParentStopPropergation { get; set; }
15+
[Parameter] public bool ChildStopPropergation { get; set; }
16+
public int GrandParentTriggerCount { get; private set; }
17+
public int ParentTriggerCount { get; private set; }
18+
public int ChildTriggerCount { get; private set; }
19+
20+
protected override void BuildRenderTree(RenderTreeBuilder builder)
21+
{
22+
if (EventName is null) return;
23+
24+
builder.OpenElement(0, "div");
25+
builder.AddAttribute(1, EventName, EventCallback.Factory.Create<EventArgs>(this, (evt) => GrandParentTriggerCount++));
26+
builder.AddAttribute(2, "id", "grand-parent");
27+
builder.AddEventStopPropagationAttribute(3, EventName, GrandParentStopPropergation);
28+
{
29+
builder.OpenElement(10, "div");
30+
builder.AddAttribute(11, EventName, EventCallback.Factory.Create<EventArgs>(this, (evt) => ParentTriggerCount++));
31+
builder.AddAttribute(12, "id", "parent");
32+
builder.AddEventStopPropagationAttribute(13, EventName, ParentStopPropergation);
33+
{
34+
builder.OpenElement(20, ChildElementType);
35+
{
36+
builder.AddAttribute(21, EventName, EventCallback.Factory.Create<EventArgs>(this, (evt) => ChildTriggerCount++));
37+
builder.AddAttribute(22, "id", "child");
38+
builder.AddEventStopPropagationAttribute(23, EventName, ChildStopPropergation);
39+
if (ChildElementDisabled)
40+
{
41+
builder.AddAttribute(24, "disabled", "disabled");
42+
}
43+
}
44+
builder.CloseElement();
45+
}
46+
builder.CloseElement();
47+
}
48+
builder.CloseElement();
49+
}
50+
}
51+
}

tests/bunit.web.tests/EventDispatchExtensions/ClipboardEventDispatchExtensionsTest.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System.Reflection;
2-
using System.Threading.Tasks;
32
using Microsoft.AspNetCore.Components.Web;
43
using Xunit;
54

tests/bunit.web.tests/EventDispatchExtensions/DragEventDispatchExtensionsTest.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System.Reflection;
2-
using System.Threading.Tasks;
32
using Microsoft.AspNetCore.Components.Web;
43
using Xunit;
54

tests/bunit.web.tests/EventDispatchExtensions/EventDispatchExtensionsTest.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using System.Diagnostics.CodeAnalysis;
44
using System.Linq;
55
using System.Reflection;
6-
using System.Threading.Tasks;
76
using AngleSharp.Dom;
87
using Bunit.TestAssets.SampleComponents;
98
using Shouldly;

tests/bunit.web.tests/EventDispatchExtensions/FocusEventDispatchExtensionsTest.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System.Reflection;
2-
using System.Threading.Tasks;
32
using Microsoft.AspNetCore.Components.Web;
43
using Xunit;
54

0 commit comments

Comments
 (0)