Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ All notable changes to **bUnit** will be documented in this file. The project ad

## [Unreleased]

### Changed
### Fixed

- Some characters where not properly escaped. Reported by [@pwhe23](https://github.com/pwhe23). Fixed by [@linkdotnet](https://github.com/linkdotnet).
- Clicking a submit button or submit input element inside a form, submits the form, if the submit button or submit input element does not have the `@onclick:preventDefault` attribute set. Reported by [@linkdotnet](https://github.com/linkdotnet). Fixed by [@egil](https://github.com/egil).

## [1.17.2] - 2023-02-22

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,14 @@ public static void TriggerEvent(this IElement element, string eventName, EventAr
public static Task TriggerEventAsync(this IElement element, string eventName, EventArgs eventArgs)
{
if (element is null)
{
throw new ArgumentNullException(nameof(element));
}

if (eventName is null)
{
throw new ArgumentNullException(nameof(eventName));
}

var renderer = element.GetTestContext()?.Renderer
?? throw new InvalidOperationException($"Blazor events can only be raised on elements rendered with the Blazor test renderer '{nameof(ITestRenderer)}'.");
Expand All @@ -80,49 +85,41 @@ public static Task TriggerEventAsync(this IElement element, string eventName, Ev
}

[SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "HTML events are standardize to lower case and safe in this context.")]
private static async Task TriggerEventsAsync(ITestRenderer renderer, IElement element, string eventName, EventArgs eventArgs)
private static Task TriggerEventsAsync(ITestRenderer renderer, IElement element, string eventName, EventArgs eventArgs)
{
var isNonBubblingEvent = NonBubblingEvents.Contains(eventName.ToLowerInvariant());
var unwrappedElement = element.Unwrap();
if (isNonBubblingEvent)
await TriggerNonBubblingEventAsync(renderer, unwrappedElement, eventName, eventArgs);
else
await TriggerBubblingEventAsync(renderer, unwrappedElement, eventName, eventArgs);

switch (unwrappedElement)
{
case IHtmlInputElement { Type: "submit", Form: not null } input when eventName is "onclick":
await TriggerFormSubmitAsync(renderer, input, eventArgs, input.Form);
break;
case IHtmlButtonElement { Type: "submit", Form: not null } button when eventName is "onclick":
await TriggerFormSubmitAsync(renderer, button, eventArgs, button.Form);
break;
}
return isNonBubblingEvent
? TriggerNonBubblingEventAsync(renderer, unwrappedElement, eventName, eventArgs)
: TriggerBubblingEventAsync(renderer, unwrappedElement, eventName, eventArgs);
}

private static Task TriggerFormSubmitAsync(ITestRenderer renderer, IElement element, EventArgs eventArgs, IHtmlFormElement form)
private static Task TriggerNonBubblingEventAsync(ITestRenderer renderer, IElement element, string eventName, EventArgs eventArgs)
{
const string eventName = "onclick";

var eventAttrName = Htmlizer.ToBlazorAttribute(eventName);
var preventDefaultAttrName = $"{eventAttrName}:preventdefault";
if (element.HasAttribute(preventDefaultAttrName))
return Task.CompletedTask;

var events = GetDispatchEventTasks(renderer, form, "onsubmit", eventArgs);
if (string.Equals(eventName, "onsubmit", StringComparison.Ordinal) && element is not IHtmlFormElement)
{
throw new InvalidOperationException("Only forms can have a onsubmit event");
}

if (events.Count == 0)
throw new MissingEventHandlerException(element, eventName);
if (element.TryGetEventId(eventAttrName, out var id))
{
return renderer.DispatchEventAsync(id, new EventFieldInfo { FieldValue = eventName }, eventArgs);
}

return Task.WhenAll(events);
throw new MissingEventHandlerException(element, eventName);
}

private static Task TriggerBubblingEventAsync(ITestRenderer renderer, IElement element, string eventName, EventArgs eventArgs)
{
var eventTasks = GetDispatchEventTasks(renderer, element, eventName, eventArgs);

if (eventTasks.Count == 0)
{
throw new MissingEventHandlerException(element, eventName);
}

return Task.WhenAll(eventTasks);
}
Expand All @@ -139,10 +136,17 @@ private static List<Task> GetDispatchEventTasks(

foreach (var candidate in element.GetParentsAndSelf())
{
if (candidate.TryGetEventId(eventAttrName, out var id))
if (candidate.TryGetEventId(eventAttrName, out var eventId))
{
var info = new EventFieldInfo { FieldValue = eventName };
eventTasks.Add(renderer.DispatchEventAsync(id, info, eventArgs, ignoreUnknownEventHandlers: eventTasks.Count > 0));
eventTasks.Add(renderer.DispatchEventAsync(eventId, info, eventArgs, ignoreUnknownEventHandlers: eventTasks.Count > 0));
}

// Special case for elements inside form elements
if (TryGetParentFormElementSpecialCase(candidate, eventName, out var formEventId))
{
var info = new EventFieldInfo { FieldValue = "onsubmit" };
eventTasks.Add(renderer.DispatchEventAsync(formEventId, info, eventArgs, ignoreUnknownEventHandlers: true));
}

if (candidate.HasAttribute(eventStopPropagationAttrName) || candidate.EventIsDisabled(eventName))
Expand All @@ -154,34 +158,46 @@ private static List<Task> GetDispatchEventTasks(
return eventTasks;
}

private static Task TriggerNonBubblingEventAsync(ITestRenderer renderer, IElement element, string eventName, EventArgs eventArgs)
private static bool TryGetParentFormElementSpecialCase(
IElement element,
string eventName,
out ulong eventId)
{
var eventAttrName = Htmlizer.ToBlazorAttribute(eventName);
eventId = default;

if (string.Equals(eventName, "onsubmit", StringComparison.Ordinal) && element is not IHtmlFormElement)
throw new InvalidOperationException("Only forms can have a onsubmit event");
if (!eventName.Equals("onclick", StringComparison.OrdinalIgnoreCase))
{
return false;
}

if (element.TryGetEventId(eventAttrName, out var id))
return renderer.DispatchEventAsync(id, new EventFieldInfo { FieldValue = eventName }, eventArgs);
var eventAttrName = Htmlizer.ToBlazorAttribute(eventName);
var preventDefaultAttrName = $"{eventAttrName}:preventdefault";
if (element.HasAttribute(preventDefaultAttrName))
{
return false;
}

throw new MissingEventHandlerException(element, eventName);
var form = element switch
{
IHtmlInputElement { Type: "submit", Form: not null } input => input.Form,
IHtmlButtonElement { Type: "submit", Form: not null } button => button.Form,
_ => null
};

return form is not null
&& form.TryGetEventId(Htmlizer.ToBlazorAttribute("onsubmit"), out eventId);
}

private static bool EventIsDisabled(this IElement element, string eventName)
{
// We want to replicate the normal DOM event behavior that, for 'interactive' elements
// with a 'disabled' attribute, certain mouse events are suppressed

switch (element)
return element switch
{
case IHtmlButtonElement:
case IHtmlInputElement:
case IHtmlTextAreaElement:
case IHtmlSelectElement:
return DisabledEventNames.Contains(eventName) && element.IsDisabled();
default:
return false;
}
IHtmlButtonElement or IHtmlInputElement or IHtmlTextAreaElement or IHtmlSelectElement => DisabledEventNames.Contains(eventName) && element.IsDisabled(),
_ => false,
};
}

private static bool TryGetEventId(this IElement element, string blazorEventName, out ulong id)
Expand Down
10 changes: 10 additions & 0 deletions tests/bunit.testassets/SampleComponents/ButtonsInsideForm.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<form id="my-form">
<button id="inside-form-button" type="submit" @onclick="() => Clicked = true">Submit</button>
<input id="inside-form-input" type="submit" @onclick="() => Clicked = true" value="Submit" />
<button type="submit">
<span id="span-inside-form-button" @onclick="() => Clicked = true"></span>
</button>
</form>
@code {
public bool Clicked;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
<form id="my-form" @onsubmit="() => FormSubmitted = true">
<button id="inside-form-button" type="submit" @onclick="() => Clicked = true" @onclick:preventDefault=PreventDefault>Submit</button>
<input id="inside-form-input" type="submit" @onclick="() => Clicked = true" @onclick:preventDefault=PreventDefault value="Submit" />
<button id="inside-form-button-no-handler" type="submit" >Submit</button>
<input id="inside-form-input-no-handler" type="submit" />
<button type="submit" @onclick="() => Clicked = true" @onclick:preventDefault=PreventDefault>
<span id="span-inside-form-button"></span>
</button>
<button type="submit">
<span id="span-inside-form-button-no-handler"></span>
</button>
</form>

<button id="outside-form-button" form="my-form">Submit outside</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@ public class GeneralEventDispatchExtensionsTest : EventDispatchExtensionsTest<Ev
public void CanRaiseEvents(MethodInfo helper)
{
if (helper is null)
{
throw new ArgumentNullException(nameof(helper));
}

if (helper.Name == nameof(TriggerEventDispatchExtensions.TriggerEventAsync))
{
return;
}

VerifyEventRaisesCorrectly(helper, EventArgs.Empty);
}
Expand Down Expand Up @@ -266,18 +270,50 @@ public void Test306()
Should.Throw<InvalidOperationException>(() => cut.Find("button").Submit());
}

[Fact(DisplayName = "Should not submit a form if the button has preventDefault")]
public void Test307()
[Theory(DisplayName = "Should not submit a form if the button has preventDefault")]
[InlineData("#inside-form-input")]
[InlineData("#inside-form-button")]
[InlineData("#span-inside-form-button")]
public void Test307(string submitElementSelector)
{
var cut = RenderComponent<SubmitFormOnClick>(
ComponentParameter.CreateParameter(nameof(SubmitFormOnClick.PreventDefault), true));
var cut = RenderComponent<SubmitFormOnClick>(ps => ps
.Add(x => x.PreventDefault, true));

cut.Find("#inside-form-input").Click();
cut.Find(submitElementSelector).Click();

cut.Instance.FormSubmitted.ShouldBeFalse();
cut.Instance.Clicked.ShouldBeTrue();
}

[Theory(DisplayName = "Should submit a form when submit button clicked")]
[InlineData("#inside-form-input")]
[InlineData("#inside-form-button")]
[InlineData("#span-inside-form-button")]
[InlineData("#inside-form-input-no-handler")]
[InlineData("#inside-form-button-no-handler")]
[InlineData("#span-inside-form-button-no-handler")]
public void Test308(string submitElementSelector)
{
var cut = RenderComponent<SubmitFormOnClick>();

cut.Find(submitElementSelector).Click();

cut.Instance.FormSubmitted.ShouldBeTrue();
}

[Theory(DisplayName = "Should trigger click handler of buttons inside form")]
[InlineData("#inside-form-input")]
[InlineData("#inside-form-button")]
[InlineData("#span-inside-form-button")]
public void Test309(string submitElementSelector)
{
var cut = RenderComponent<ButtonsInsideForm>();

cut.Find(submitElementSelector).Click();

cut.Instance.Clicked.ShouldBeTrue();
}

public static IEnumerable<object[]> GetTenNumbers() => Enumerable.Range(0, 10)
.Select(i => new object[] { i });

Expand Down