diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ac8e4790..4524e166c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ For a migration guide, see [Upgrading bUnit](https://bunit.dev/docs/migrations/i ### Added +- Support for form submission from submit buttons and inputs that are outside the form element but associated via the HTML5 `form` attribute. Reported and fixed in [#1766](https://github.com/bUnit-dev/bUnit/issues/1766). - Improved renderer logic that catches more edge cases. - Improved developer experience in relation to JSInterop. diff --git a/src/bunit/EventDispatchExtensions/TriggerEventDispatchExtensions.cs b/src/bunit/EventDispatchExtensions/TriggerEventDispatchExtensions.cs index d5dc40768..637e8b50a 100644 --- a/src/bunit/EventDispatchExtensions/TriggerEventDispatchExtensions.cs +++ b/src/bunit/EventDispatchExtensions/TriggerEventDispatchExtensions.cs @@ -176,10 +176,56 @@ private static bool TryGetParentFormElementSpecialCase( IHtmlButtonElement { Type: "submit", Form: not null } button => button.Form, _ => null }; + + // If form is still null, try to find it by the form attribute for submit buttons/inputs + if (form is null && element.HasAttribute("form")) + { + var isSubmitElement = element switch + { + IHtmlInputElement { Type: "submit" } => true, + IHtmlButtonElement { Type: "submit" } => true, + _ => false + }; + + if (isSubmitElement) + { + var formId = element.GetAttribute("form"); + if (!string.IsNullOrEmpty(formId)) + { + // Try to find the form element by traversing up to find a common ancestor + // and then searching down for the form + form = FindFormById(element, formId); + } + } + } return form is not null && form.TryGetEventId(Htmlizer.ToBlazorAttribute("onsubmit"), out eventId); } + + private static IHtmlFormElement? FindFormById(IElement element, string formId) + { + // Traverse up the DOM tree to find a common ancestor and search its children + // for the form with the matching ID. This handles cases where the button and + // form are siblings or in different subtrees. + var current = element.Parent as IElement; + while (current is not null) + { + // Search children of current element for the form with matching ID + foreach (var child in current.Children) + { + if ((child.Id == formId || child.GetAttribute("id") == formId) && child is IHtmlFormElement htmlForm) + { + return htmlForm; + } + } + + // Move up to parent to widen the search + current = current.Parent as IElement; + } + + return null; + } private static bool EventIsDisabled(this IElement element, string eventName) { diff --git a/tests/bunit.testassets/SampleComponents/FormWithButtonOutside.razor b/tests/bunit.testassets/SampleComponents/FormWithButtonOutside.razor new file mode 100644 index 000000000..cbd47675f --- /dev/null +++ b/tests/bunit.testassets/SampleComponents/FormWithButtonOutside.razor @@ -0,0 +1,12 @@ +
+ + +@code { + public bool SubmitWasCalled { get; private set; } + + private void Callback() + { + SubmitWasCalled = true; + } +} diff --git a/tests/bunit.testassets/SampleComponents/FormWithSubmitElementsOutside.razor b/tests/bunit.testassets/SampleComponents/FormWithSubmitElementsOutside.razor new file mode 100644 index 000000000..869125474 --- /dev/null +++ b/tests/bunit.testassets/SampleComponents/FormWithSubmitElementsOutside.razor @@ -0,0 +1,13 @@ + + + + +@code { + public bool SubmitWasCalled { get; private set; } + + private void Callback() + { + SubmitWasCalled = true; + } +} diff --git a/tests/bunit.tests/EventDispatchExtensions/FormDispatchExtensionTest.cs b/tests/bunit.tests/EventDispatchExtensions/FormDispatchExtensionTest.cs index 70337a6fb..7ae88ee38 100644 --- a/tests/bunit.tests/EventDispatchExtensions/FormDispatchExtensionTest.cs +++ b/tests/bunit.tests/EventDispatchExtensions/FormDispatchExtensionTest.cs @@ -6,29 +6,59 @@ public class FormDispatchExtensionTest : BunitContext public void ClickingOnSubmitButtonTriggersOnsubmitOfForm() { var cut = Render