From 7ca89e9d5a3aca5a99379b9d757604032e539e5f Mon Sep 17 00:00:00 2001 From: Egil Hansen Date: Fri, 31 Oct 2025 15:23:56 +0000 Subject: [PATCH 1/3] test: create clicking on submit button outside triggers onsubmit of form --- .../FormWithButtonOutside.razor | 12 +++++++++++ .../FormDispatchExtensionTest.cs | 20 ++++++++++++++----- 2 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 tests/bunit.testassets/SampleComponents/FormWithButtonOutside.razor 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.tests/EventDispatchExtensions/FormDispatchExtensionTest.cs b/tests/bunit.tests/EventDispatchExtensions/FormDispatchExtensionTest.cs index 70337a6fb..226affdbd 100644 --- a/tests/bunit.tests/EventDispatchExtensions/FormDispatchExtensionTest.cs +++ b/tests/bunit.tests/EventDispatchExtensions/FormDispatchExtensionTest.cs @@ -6,29 +6,39 @@ public class FormDispatchExtensionTest : BunitContext public void ClickingOnSubmitButtonTriggersOnsubmitOfForm() { var cut = Render(p => p.Add(s => s.HasSubmitType, true)); - + cut.Find("#submitter").Click(); cut.Instance.SubmitWasCalled.ShouldBeTrue(); } - + [Fact] public void ButtonWithoutTypeIsConsideredSubmitAndTriggersOnsubmitOfForm() { var cut = Render(p => p.Add(s => s.HasSubmitType, false)); - + cut.Find("#submitter").Click(); cut.Instance.SubmitWasCalled.ShouldBeTrue(); } - + [Fact] public void ButtonThatIsNotSubmitShouldNotTrigger() { var cut = Render(p => p.Add(s => s.HasSubmitType, false)); - + cut.Find("#other").Click(); cut.Instance.SubmitWasCalled.ShouldBeFalse(); } + + [Fact] + public void ClickingOnSubmitButtonOutsideTriggersOnsubmitOfForm() + { + var cut = Render(); + + cut.Find("#submitter").Click(); + + cut.Instance.SubmitWasCalled.ShouldBeTrue(); + } } From b270f6ab7f876fc2d7266e670092ed0f45ee89f7 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 07:55:06 +0000 Subject: [PATCH 2/3] feat: Support form submission from buttons and inputs outside form using form attribute (#1771) * Initial plan * Initial investigation of form submit issue Co-authored-by: egil <105649+egil@users.noreply.github.com> * Implement support for clicking submit button outside form with form attribute Co-authored-by: egil <105649+egil@users.noreply.github.com> * Improve comments in FindFormById method Co-authored-by: egil <105649+egil@users.noreply.github.com> * Restore net10.0 target framework to all projects Co-authored-by: linkdotnet <26365461+linkdotnet@users.noreply.github.com> * Fix misleading comment about incremental DOM rendering Co-authored-by: egil <105649+egil@users.noreply.github.com> * Add test cases for input element with form attribute outside form Co-authored-by: egil <105649+egil@users.noreply.github.com> * Simplify FindFormById by removing GetElementById attempt Co-authored-by: egil <105649+egil@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: egil <105649+egil@users.noreply.github.com> Co-authored-by: linkdotnet <26365461+linkdotnet@users.noreply.github.com> --- .../TriggerEventDispatchExtensions.cs | 46 +++++++++++++++++++ .../FormWithSubmitElementsOutside.razor | 13 ++++++ .../FormDispatchExtensionTest.cs | 20 ++++++++ 3 files changed, 79 insertions(+) create mode 100644 tests/bunit.testassets/SampleComponents/FormWithSubmitElementsOutside.razor 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/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 226affdbd..7ae88ee38 100644 --- a/tests/bunit.tests/EventDispatchExtensions/FormDispatchExtensionTest.cs +++ b/tests/bunit.tests/EventDispatchExtensions/FormDispatchExtensionTest.cs @@ -41,4 +41,24 @@ public void ClickingOnSubmitButtonOutsideTriggersOnsubmitOfForm() cut.Instance.SubmitWasCalled.ShouldBeTrue(); } + + [Fact] + public void ClickingOnSubmitButtonOutsideWithFormAttributeTriggersOnsubmit() + { + var cut = Render(); + + cut.Find("#submit-button").Click(); + + cut.Instance.SubmitWasCalled.ShouldBeTrue(); + } + + [Fact] + public void ClickingOnSubmitInputOutsideWithFormAttributeTriggersOnsubmit() + { + var cut = Render(); + + cut.Find("#submit-input").Click(); + + cut.Instance.SubmitWasCalled.ShouldBeTrue(); + } } From 4ea058417daedc12594d0aeb39e14c4690b30af9 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 08:02:25 +0000 Subject: [PATCH 3/3] docs: Update CHANGELOG.md for form attribute support (#1772) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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.