Skip to content

Commit 622819d

Browse files
linkdotnetegil
andauthored
Clicking on submit button will trigger form submit (#580)
* Dispatch onsubmit event for forms when clicking button inside the form * Should throw exception when invoking onsubmit on non-form element * Fix MA0006 * Remove whitespaces Co-authored-by: Egil Hansen <[email protected]> * Simplified tests * Also don't consider Async version in exclusion list * Updated changelog Co-authored-by: Egil Hansen <[email protected]>
1 parent 7c8de44 commit 622819d

File tree

6 files changed

+87
-12
lines changed

6 files changed

+87
-12
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ This release reintroduces `Stub<TComponent>` and related back into the main libr
1616
### Fixed
1717

1818
- Changed `SetParametersAndRender` such that it rethrows any exceptions thrown by the component under tests `SetParametersAsync` method. Thanks to [@bonsall](https://github.com/bonsall) for reporting the issue. Fixed by [@egil](https://github.com/egil).
19+
- `onclick` on a button inside a form will raise the `onsubmit` event for the form itself. Reported by [@egil]. Fixed by [@linkdotnet](https://github.com/linkdotnet).
20+
- Only forms are allowed to have a `onsubmit` event handler. When `onsubmit` is invoked from a non-form element results in an exception. Fixed by [@linkdotnet](https://github.com/linkdotnet).
1921

2022
## [1.3.42] - 2021-11-09
2123

src/bunit.web/EventDispatchExtensions/TriggerEventDispatchExtensions.cs

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,45 @@ public static Task TriggerEventAsync(this IElement element, string eventName, Ev
4545

4646
var isNonBubblingEvent = NonBubblingEvents.Contains(eventName.ToLowerInvariant());
4747

48+
var unwrappedElement = element.Unwrap();
4849
if (isNonBubblingEvent)
49-
return TriggerNonBubblingEventAsync(renderer, element.Unwrap(), eventName, eventArgs);
50+
return TriggerNonBubblingEventAsync(renderer, unwrappedElement, eventName, eventArgs);
5051

51-
return TriggerBubblingEventAsync(renderer, element.Unwrap(), eventName, eventArgs);
52+
return unwrappedElement switch
53+
{
54+
IHtmlInputElement { Type: "submit", Form: not null } input when eventName is "onclick" =>
55+
TriggerFormSubmitBubblingEventAsync(renderer, input, eventArgs, input.Form),
56+
IHtmlButtonElement { Type: "submit", Form: not null } button when eventName is "onclick" =>
57+
TriggerFormSubmitBubblingEventAsync(renderer, button, eventArgs, button.Form),
58+
_ => TriggerBubblingEventAsync(renderer, unwrappedElement, eventName, eventArgs)
59+
};
60+
}
61+
62+
private static Task TriggerFormSubmitBubblingEventAsync(ITestRenderer renderer, IElement element, EventArgs eventArgs, IHtmlFormElement form)
63+
{
64+
var events = GetDispatchEventTasks(renderer, element, "onclick", eventArgs);
65+
events = events.Concat(GetDispatchEventTasks(renderer, form, "onsubmit", eventArgs)).ToList();
66+
67+
if (events.Count == 0)
68+
throw new MissingEventHandlerException(element, "onclick");
69+
70+
return Task.WhenAll(events);
5271
}
5372

5473
private static Task TriggerBubblingEventAsync(ITestRenderer renderer, IElement element, string eventName, EventArgs eventArgs)
74+
{
75+
var eventTasks = GetDispatchEventTasks(renderer, element, eventName, eventArgs);
76+
77+
if (eventTasks.Count == 0)
78+
throw new MissingEventHandlerException(element, eventName);
79+
80+
return Task.WhenAll(eventTasks);
81+
}
82+
83+
private static List<Task> GetDispatchEventTasks(ITestRenderer renderer, IElement element, string eventName, EventArgs eventArgs)
5584
{
5685
var eventAttrName = Htmlizer.ToBlazorAttribute(eventName);
57-
var eventStopPropergationAttrName = $"{eventAttrName}:stoppropagation";
86+
var eventStopPropagationAttrName = $"{eventAttrName}:stoppropagation";
5887
var eventTasks = new List<Task>();
5988

6089
foreach (var candidate in element.GetParentsAndSelf())
@@ -73,22 +102,22 @@ private static Task TriggerBubblingEventAsync(ITestRenderer renderer, IElement e
73102
}
74103
}
75104

76-
if (candidate.HasAttribute(eventStopPropergationAttrName) || candidate.EventIsDisabled(eventName))
105+
if (candidate.HasAttribute(eventStopPropagationAttrName) || candidate.EventIsDisabled(eventName))
77106
{
78107
break;
79108
}
80109
}
81110

82-
if (eventTasks.Count == 0)
83-
throw new MissingEventHandlerException(element, eventName);
84-
85-
return Task.WhenAll(eventTasks);
111+
return eventTasks;
86112
}
87113

88114
private static Task TriggerNonBubblingEventAsync(ITestRenderer renderer, IElement element, string eventName, EventArgs eventArgs)
89115
{
90116
var eventAttrName = Htmlizer.ToBlazorAttribute(eventName);
91117

118+
if (string.Equals(eventName, "onsubmit", StringComparison.Ordinal) && element is not IHtmlFormElement)
119+
throw new InvalidOperationException("Only forms can have a onsubmit event");
120+
92121
if (element.TryGetEventId(eventAttrName, out var id))
93122
return renderer.DispatchEventAsync(id, new EventFieldInfo() { FieldValue = eventName }, eventArgs);
94123

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<form @onsubmit="() => { }">
2+
<button @onsubmit="() => { }"></button>
3+
</form>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<form id="my-form" @onsubmit="() => FormSubmitted = true">
2+
<button id="inside-form-button" type="submit" @onclick="() => Clicked = true">Submit</button>
3+
<input id="inside-form-input" type="submit" @onclick="() => Clicked = true" value="Submit" />
4+
</form>
5+
6+
<button id="outside-form-button" form="my-form">Submit outside</button>
7+
8+
@code {
9+
public bool FormSubmitted;
10+
public bool Clicked;
11+
}

tests/bunit.web.tests/EventDispatchExtensions/GeneralEventDispatchExtensionsTest.cs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ public void Test101()
9090
[InlineData("onprogress")]
9191
[InlineData("onreset")]
9292
[InlineData("onscroll")]
93-
[InlineData("onsubmit")]
9493
[InlineData("onunload")]
9594
[InlineData("ontoggle")]
9695
[InlineData("onDOMNodeInsertedIntoDocument")]
@@ -232,5 +231,35 @@ public void Test303(string exceptionMessage)
232231
Should.Throw<Exception>(() => cut.Find("button").Click())
233232
.Message.ShouldBe(exceptionMessage);
234233
}
234+
235+
[Fact(DisplayName = "Should handle click event first and submit form afterwards for button")]
236+
public void Test304()
237+
{
238+
var cut = RenderComponent<SubmitFormOnClick>();
239+
240+
cut.Find("button").Click();
241+
242+
cut.Instance.FormSubmitted.ShouldBeTrue();
243+
cut.Instance.Clicked.ShouldBeTrue();
244+
}
245+
246+
[Fact(DisplayName = "Should handle click event first and submit form afterwards for input when type button")]
247+
public void Test305()
248+
{
249+
var cut = RenderComponent<SubmitFormOnClick>();
250+
251+
cut.Find("#inside-form-input").Click();
252+
253+
cut.Instance.FormSubmitted.ShouldBeTrue();
254+
cut.Instance.Clicked.ShouldBeTrue();
255+
}
256+
257+
[Fact(DisplayName = "Should throw exception when invoking onsubmit from non form")]
258+
public void Test306()
259+
{
260+
var cut = RenderComponent<OnsubmitButton>();
261+
262+
Should.Throw<InvalidOperationException>(() => cut.Find("button").Submit());
263+
}
235264
}
236265
}

tests/bunit.web.tests/EventDispatchExtensions/InputEventDispatchExtensionsTest.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ namespace Bunit
22
{
33
public class InputEventDispatchExtensionsTest : EventDispatchExtensionsTest<ChangeEventArgs>
44
{
5-
private static readonly string[] ExcludedMethodsFromGenericTests = new[] { "Change", "Input" };
6-
public static IEnumerable<object[]> Helpers { get; }
7-
= GetEventHelperMethods(typeof(InputEventDispatchExtensions), x => !ExcludedMethodsFromGenericTests.Contains(x.Name));
5+
private static readonly string[] ExcludedMethodsFromGenericTests = new[] { "Change", "Input", "Submit" };
6+
public static IEnumerable<object[]> Helpers { get; }
7+
= GetEventHelperMethods(typeof(InputEventDispatchExtensions), x => !ExcludedMethodsFromGenericTests
8+
.Contains(x.Name.Replace("Async", string.Empty, StringComparison.Ordinal)));
89

910
private static readonly Fixture Fixture = new();
1011

0 commit comments

Comments
 (0)