This is an automated email from the ASF dual-hosted git repository.

reiern70 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/wicket.git


The following commit(s) were added to refs/heads/master by this push:
     new 2c89c796ce Keep Firefox from submitting a form for the triggered 
submit event
2c89c796ce is described below

commit 2c89c796cecd7e5b26925f804876edd867b4f7e7
Author: Ernesto Reinaldo Barreiro <[email protected]>
AuthorDate: Tue Oct 6 10:39:20 2026 -0500

    Keep Firefox from submitting a form for the triggered submit event
    
    An AjaxFormSubmitBehavior whose shouldTriggerJavaScriptSubmitEvent() returns
    true fires a submit event on the form before its Ajax request, so that 
submit
    handlers run - UploadProgressBar starts its bar from one. Since the 
jQuery-free
    engine, the precondition fired it with dispatchEvent(). Firefox still 
submits
    a form for a submit event fired by a script unless the event is cancelled, 
so
    an AjaxButton like the one of the upload example sent its Ajax request and
    submitted the form the normal way as well: the page was reloaded behind the
    Ajax response. Chrome does not submit for such an event.
    
    The precondition now calls the new Wicket.Event.triggerSubmit(form) of both
    engines. It fires the event and tells whether a submit handler cancelled it.
    A listener added last on the form cancels the event, as the jQuery code 
before
    the jQuery-free engine did, so no browser submits the form; the Ajax request
    does. The event is also cancelled when a handler stops its immediate
    propagation, and when a capturing handler on an ancestor stops its
    propagation before it reaches the form. A handler on the form, or one
    capturing the event on its way there, can still cancel the Ajax request,
    also after an earlier handler on the form stopped the propagation. Handlers
    above the form see the event cancelled already, so a document level listener
    no longer stops the Ajax request, as before the jQuery-free engine.
    
    Form.getJsForListenerUrl() fired a submit event through Wicket.Event.fire()
    to submit the form. With the jQuery-free engine that is a plain
    dispatchEvent(), for which Chrome never submits a form, so a
    FormComponentUpdatingBehavior did nothing in Chrome. It now calls
    triggerSubmit() and submits the form unless a handler cancelled the event, 
in
    both engines, which is what jQuery's trigger() did; listeners added with
    addEventListener() see that event with the jQuery engine too. The form is
    submitted through HTMLFormElement.prototype.submit, so a form control named
    "submit", which shadows the form's submit(), no longer breaks it.
    
    Applications see the Ajax request alone, in Firefox as in Chrome, and their
    forms submitted for a listener URL whatever their controls are named.
    Wicket 10.x is not affected: its precondition triggers the event through
    jQuery and cancels it.
    
    SubmitEventSeleniumTest drives the Ajax upload and form input examples with
    both engines, and can run in Firefox or on a Selenium grid.
    
    GitHub issue #1650
    
    Co-authored-by: Emond Papegaaij <[email protected]>
---
 JAVASCRIPTTESTING.md                               |  13 ++
 .../ajax/form/AjaxFormSubmitBehaviorTest.java      |  27 +++
 .../apache/wicket/markup/html/form/FormTest.java   |   4 +-
 .../wicket/ajax/form/AjaxFormSubmitBehavior.java   |   7 +-
 .../wicket/ajax/res/js/wicket-ajax-jquery.js       |  49 +++++
 .../org/apache/wicket/ajax/res/js/wicket-ajax.js   |  49 +++++
 .../org/apache/wicket/markup/html/form/Form.java   |   6 +-
 wicket-core/src/test/js/event.js                   | 207 +++++++++++++++++++++
 .../wicket/examples/SubmitEventSeleniumTest.java   | 193 +++++++++++++++++++
 9 files changed, 551 insertions(+), 4 deletions(-)

diff --git a/JAVASCRIPTTESTING.md b/JAVASCRIPTTESTING.md
index 19edaa1801..245430b585 100644
--- a/JAVASCRIPTTESTING.md
+++ b/JAVASCRIPTTESTING.md
@@ -115,3 +115,16 @@ mvn verify -pl wicket-examples -Dwicket.selenium=true 
-Dtest=VeilPageSeleniumTes
 
 Selenium Manager, which ships with Selenium, finds a matching ChromeDriver, 
and downloads
 Chrome for Testing too when no Chrome is installed, so the first run needs 
network access.
+
+`SubmitEventSeleniumTest` checks the forms that fire a `submit` event from a 
script, which
+Firefox and Chrome handle differently, so it is worth running in both. It 
takes the browser,
+and optionally a Selenium grid to run it on, from system properties - for 
example against the
+`selenium/standalone-firefox` Docker image:
+
+```bash
+docker run -d --rm -p 4444:4444 --add-host host.docker.internal:host-gateway \
+       --shm-size 2g selenium/standalone-firefox
+mvn verify -pl wicket-examples -Dwicket.selenium=true 
-Dtest=SubmitEventSeleniumTest \
+       -Dwicket.selenium.browser=firefox 
-Dwicket.selenium.remote=http://localhost:4444 \
+       -Dwicket.selenium.host=host.docker.internal
+```
diff --git 
a/wicket-core-tests/src/test/java/org/apache/wicket/ajax/form/AjaxFormSubmitBehaviorTest.java
 
b/wicket-core-tests/src/test/java/org/apache/wicket/ajax/form/AjaxFormSubmitBehaviorTest.java
index 48d5007cc1..f7a85e844a 100644
--- 
a/wicket-core-tests/src/test/java/org/apache/wicket/ajax/form/AjaxFormSubmitBehaviorTest.java
+++ 
b/wicket-core-tests/src/test/java/org/apache/wicket/ajax/form/AjaxFormSubmitBehaviorTest.java
@@ -78,6 +78,33 @@ class AjaxFormSubmitBehaviorTest extends WicketTestCase
                formTester.submit("submit");
        }
 
+       /**
+        * The submit event is triggered through {@code 
Wicket.Event.triggerSubmit()}, which keeps
+        * Firefox from submitting the form for it besides the Ajax request.
+        */
+       @Test
+       void theSubmitEventIsTriggeredWithoutSubmittingTheForm()
+       {
+               AjaxFormSubmitBehaviorTestPage page = new 
AjaxFormSubmitBehaviorTestPage();
+               page.getForm().getTextField().add(new 
AjaxFormSubmitBehavior("click")
+               {
+                       private static final long serialVersionUID = 1L;
+
+                       @Override
+                       protected boolean shouldTriggerJavaScriptSubmitEvent()
+                       {
+                               return true;
+                       }
+               });
+
+               tester.startPage(page);
+
+               String response = tester.getLastResponseAsString();
+               
assertTrue(response.contains("Wicket.Event.triggerSubmit(document.getElementById('"
 +
+                       page.getForm().getMarkupId() + "'))"), response);
+               assertFalse(response.contains("dispatchEvent"), response);
+       }
+
        public static class NestedFormTestPage extends NestedFormPage
        {
                
diff --git 
a/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/FormTest.java
 
b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/FormTest.java
index 919bc8eddb..65c7cca929 100644
--- 
a/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/FormTest.java
+++ 
b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/FormTest.java
@@ -85,11 +85,11 @@ public class FormTest extends WicketTestCase
                FormMethodTestPage page = 
(FormMethodTestPage)tester.getLastRenderedPage();
        
                // form action contains path and query 
-               assertEquals("var f = 
document.getElementById('formpost1');f.action='path?param1&param2=val%20ue';Wicket.Event.fire(f,
 'submit');",
+               assertEquals("var f = 
document.getElementById('formpost1');f.action='path?param1&param2=val%20ue';if 
(Wicket.Event.triggerSubmit(f)) { HTMLFormElement.prototype.submit.call(f); }",
                        
page.postForm.getJsForListenerUrl("path?param1&param2=val%20ue").toString());
                
                // form action must not contain query (since ignored by 
browser), put into hidden form parameters instead 
-               
assertEquals("document.getElementById('formget2_hf_0').innerHTML = '<input 
type=\"hidden\" name=\"param1\" value=\"\" /><input type=\"hidden\" 
name=\"param2\" value=\"val ue\" />';var f = 
document.getElementById('formget2');f.action='path';Wicket.Event.fire(f, 
'submit');",
+               
assertEquals("document.getElementById('formget2_hf_0').innerHTML = '<input 
type=\"hidden\" name=\"param1\" value=\"\" /><input type=\"hidden\" 
name=\"param2\" value=\"val ue\" />';var f = 
document.getElementById('formget2');f.action='path';if 
(Wicket.Event.triggerSubmit(f)) { HTMLFormElement.prototype.submit.call(f); }",
                        
page.getForm.getJsForListenerUrl("path?param1&param2=val%20ue").toString());
        }
 
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/ajax/form/AjaxFormSubmitBehavior.java
 
b/wicket-core/src/main/java/org/apache/wicket/ajax/form/AjaxFormSubmitBehavior.java
index 128ca2a514..d35625bd9d 100644
--- 
a/wicket-core/src/main/java/org/apache/wicket/ajax/form/AjaxFormSubmitBehavior.java
+++ 
b/wicket-core/src/main/java/org/apache/wicket/ajax/form/AjaxFormSubmitBehavior.java
@@ -143,6 +143,11 @@ public abstract class AjaxFormSubmitBehavior extends 
AjaxEventBehavior
        /**
         * Controls whether or not a JS <code>submit</code> should be triggered 
on the submitting form.
         * False by default.
+        * <p>
+        * The event runs the submit handlers of the form. A handler on the 
form, or one capturing the
+        * event on its way there, cancelling it stops the Ajax request; 
handlers further up see it
+        * cancelled already. The browser does not submit the form for this 
event, the Ajax request
+        * does, also when a handler stops the propagation of the event.
         * 
         * @return true if <code>submit</code> should be triggered, false 
otherwise
         */
@@ -184,7 +189,7 @@ public abstract class AjaxFormSubmitBehavior extends 
AjaxEventBehavior
                                public CharSequence getPrecondition(Component 
component)
                                {
                                        return String.format(
-                                               "return 
document.getElementById('%s').dispatchEvent(new Event('submit', { cancelable: 
true, bubbles: true }));",
+                                               "return 
Wicket.Event.triggerSubmit(document.getElementById('%s'));",
                                                form.getMarkupId());
                                }
                        });
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/ajax/res/js/wicket-ajax-jquery.js 
b/wicket-core/src/main/java/org/apache/wicket/ajax/res/js/wicket-ajax-jquery.js
index 605c6604b0..375a15b2b0 100644
--- 
a/wicket-core/src/main/java/org/apache/wicket/ajax/res/js/wicket-ajax-jquery.js
+++ 
b/wicket-core/src/main/java/org/apache/wicket/ajax/res/js/wicket-ajax-jquery.js
@@ -2509,6 +2509,55 @@
                                }
                        },
 
+                       /**
+                        * Fires a submit event on the given form, so that its 
submit handlers run, without
+                        * submitting the form: an Ajax request submits it 
instead. Firefox submits a form for
+                        * a submit event fired by a script unless the event is 
cancelled, so the event is
+                        * cancelled once the handlers on the form have run, as 
the last listener on the form,
+                        * or as soon as a handler stops it from getting there.
+                        *
+                        * @param form {HTMLFormElement} the form
+                        * @returns {boolean} false if a handler on the form, 
or one capturing the event on its
+                        *      way there, cancelled the event
+                        */
+                       triggerSubmit: function(form) {
+                               const event = new Event('submit', { cancelable: 
true, bubbles: true });
+                               let cancelledByHandler = null;
+                               const cancel = function () {
+                                       if (cancelledByHandler === null) {
+                                               cancelledByHandler = 
event.defaultPrevented;
+                                               event.preventDefault();
+                                       }
+                               };
+                               const cancelOwnEvent = function (e) {
+                                       if (e === event) {
+                                               cancel();
+                                       }
+                               };
+                               // a handler stopping the propagation before 
the form, or the immediate propagation,
+                               // keeps cancelOwnEvent from running
+                               const stopPropagation = event.stopPropagation;
+                               event.stopPropagation = function () {
+                                       stopPropagation.call(this);
+                                       if (this.eventPhase === 
Event.CAPTURING_PHASE) {
+                                               cancel();
+                                       }
+                               };
+                               const stopImmediatePropagation = 
event.stopImmediatePropagation;
+                               event.stopImmediatePropagation = function () {
+                                       stopImmediatePropagation.call(this);
+                                       cancel();
+                               };
+                               let notCancelled;
+                               form.addEventListener('submit', cancelOwnEvent);
+                               try {
+                                       notCancelled = 
form.dispatchEvent(event);
+                               } finally {
+                                       form.removeEventListener('submit', 
cancelOwnEvent);
+                               }
+                               return cancelledByHandler === null ? 
notCancelled : !cancelledByHandler;
+                       },
+
                        /**
                         * The names of the topics on which Wicket notifies
                         */
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/ajax/res/js/wicket-ajax.js 
b/wicket-core/src/main/java/org/apache/wicket/ajax/res/js/wicket-ajax.js
index befb85810a..f1469d11ce 100644
--- a/wicket-core/src/main/java/org/apache/wicket/ajax/res/js/wicket-ajax.js
+++ b/wicket-core/src/main/java/org/apache/wicket/ajax/res/js/wicket-ajax.js
@@ -2785,6 +2785,55 @@
                        }
                },
 
+               /**
+                * Fires a submit event on the given form, so that its submit 
handlers run, without
+                * submitting the form: an Ajax request submits it instead. 
Firefox submits a form for
+                * a submit event fired by a script unless the event is 
cancelled, so the event is
+                * cancelled once the handlers on the form have run, as the 
last listener on the form,
+                * or as soon as a handler stops it from getting there.
+                *
+                * @param form {HTMLFormElement} the form
+                * @returns {boolean} false if a handler on the form, or one 
capturing the event on its
+                *      way there, cancelled the event
+                */
+               triggerSubmit: function(form) {
+                       const event = new Event('submit', { cancelable: true, 
bubbles: true });
+                       let cancelledByHandler = null;
+                       const cancel = function () {
+                               if (cancelledByHandler === null) {
+                                       cancelledByHandler = 
event.defaultPrevented;
+                                       event.preventDefault();
+                               }
+                       };
+                       const cancelOwnEvent = function (e) {
+                               if (e === event) {
+                                       cancel();
+                               }
+                       };
+                       // a handler stopping the propagation before the form, 
or the immediate propagation,
+                       // keeps cancelOwnEvent from running
+                       const stopPropagation = event.stopPropagation;
+                       event.stopPropagation = function () {
+                               stopPropagation.call(this);
+                               if (this.eventPhase === Event.CAPTURING_PHASE) {
+                                       cancel();
+                               }
+                       };
+                       const stopImmediatePropagation = 
event.stopImmediatePropagation;
+                       event.stopImmediatePropagation = function () {
+                               stopImmediatePropagation.call(this);
+                               cancel();
+                       };
+                       let notCancelled;
+                       form.addEventListener('submit', cancelOwnEvent);
+                       try {
+                               notCancelled = form.dispatchEvent(event);
+                       } finally {
+                               form.removeEventListener('submit', 
cancelOwnEvent);
+                       }
+                       return cancelledByHandler === null ? notCancelled : 
!cancelledByHandler;
+               },
+
                /**
                 * The names of the topics on which Wicket notifies
                 */
diff --git 
a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/Form.java 
b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/Form.java
index 2b959018bc..805610c3fb 100644
--- a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/Form.java
+++ b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/Form.java
@@ -519,6 +519,9 @@ public class Form<T> extends WebMarkupContainer
 
        /**
         * Generate a piece of JavaScript that submits the form to the given 
URL of an {@link IRequestListener}.
+        * <p>
+        * It fires a {@code submit} event on the form first, and submits the 
form unless a handler
+        * cancels that event. The form is submitted without client-side 
validation.
         *
         * Warning: This code should only be called in the rendering phase for 
form components inside
         * the form because it uses the css/javascript id of the form which can 
be stored in the markup.
@@ -551,7 +554,8 @@ public class Form<T> extends WebMarkupContainer
                }
                buffer.append(String.format("var f = 
document.getElementById('%s');", root.getMarkupId()));
                buffer.append(String.format("f.action='%s';", action));
-               buffer.append("Wicket.Event.fire(f, 'submit');");
+               buffer.append(
+                       "if (Wicket.Event.triggerSubmit(f)) { 
HTMLFormElement.prototype.submit.call(f); }");
                return buffer;
        }
 
diff --git a/wicket-core/src/test/js/event.js b/wicket-core/src/test/js/event.js
index fa5199f4ad..e5cfa9cc67 100644
--- a/wicket-core/src/test/js/event.js
+++ b/wicket-core/src/test/js/event.js
@@ -312,6 +312,213 @@ Wicket.Event.add(window, 'domready', function() {
        });
 
 
+       module('Wicket.Event.triggerSubmit');
+
+       // a form posting into a hidden frame, so a submission the test fails 
to stop does not leave
+       // the test page
+       const createTestForm = function (parent) {
+               const fixture = document.getElementById('qunit-fixture');
+               const frame = document.createElement('iframe');
+               frame.name = 'triggerSubmitSink';
+               fixture.appendChild(frame);
+               const form = document.createElement('form');
+               form.action = 'about:blank';
+               form.target = frame.name;
+               (parent || fixture).appendChild(form);
+               return form;
+       };
+
+       // a browser submitting the form fires formdata before dispatchEvent() 
returns
+       const recordSubmissions = function (form) {
+               const submissions = [];
+               form.addEventListener('formdata', function (event) {
+                       submissions.push(event);
+               });
+               return submissions;
+       };
+
+       const recordSubmitEvents = function (form) {
+               const events = [];
+               form.addEventListener('submit', function (event) {
+                       events.push(event);
+               });
+               return events;
+       };
+
+       test('runs the submit handlers and keeps the browser from submitting', 
assert => {
+               assert.expect(5);
+               const form = createTestForm();
+               const submissions = recordSubmissions(form);
+               form.addEventListener('submit', function (event) {
+                       assert.notOk(event.defaultPrevented, 'not cancelled 
while the handlers on the form run');
+               });
+               const events = recordSubmitEvents(form);
+
+               assert.ok(Wicket.Event.triggerSubmit(form), 'the Ajax request 
may go on');
+               assert.equal(events.length, 1, 'the handler ran');
+               assert.ok(events[0].defaultPrevented, 'the event is cancelled');
+               assert.equal(submissions.length, 0, 'the browser does not 
submit the form');
+       });
+
+       test('a handler cancelling the event stops the Ajax request', assert => 
{
+               const form = createTestForm();
+               form.onsubmit = function () {
+                       return false;
+               };
+
+               assert.notOk(Wicket.Event.triggerSubmit(form));
+       });
+
+       test('a capturing listener cancelling the event stops the Ajax 
request', assert => {
+               const form = createTestForm();
+               const cancel = function (event) {
+                       event.preventDefault();
+               };
+               document.addEventListener('submit', cancel, true);
+               try {
+                       assert.notOk(Wicket.Event.triggerSubmit(form));
+               } finally {
+                       document.removeEventListener('submit', cancel, true);
+               }
+       });
+
+       test('a listener above the form sees the event cancelled already', 
assert => {
+               const form = createTestForm();
+               let cancelled;
+               const record = function (event) {
+                       cancelled = event.defaultPrevented;
+               };
+               document.addEventListener('submit', record);
+               try {
+                       assert.ok(Wicket.Event.triggerSubmit(form), 'the Ajax 
request may go on');
+                       assert.ok(cancelled, 'the event is cancelled when it 
bubbles up');
+               } finally {
+                       document.removeEventListener('submit', record);
+               }
+       });
+
+       test('a handler stopping the propagation without cancelling lets the 
Ajax request go on', assert => {
+               const form = createTestForm();
+               const submissions = recordSubmissions(form);
+               form.addEventListener('submit', function (event) {
+                       event.stopPropagation();
+               });
+               const events = recordSubmitEvents(form);
+
+               assert.ok(Wicket.Event.triggerSubmit(form), 'the Ajax request 
may go on');
+               assert.ok(events[0].defaultPrevented, 'the event is cancelled');
+               assert.equal(submissions.length, 0, 'the browser does not 
submit the form');
+       });
+
+       test('a handler on the form cancelling after another one stopped the 
propagation stops the Ajax request', assert => {
+               const form = createTestForm();
+               form.addEventListener('submit', function (event) {
+                       event.stopPropagation();
+               });
+               let cancelledAlready;
+               form.addEventListener('submit', function (event) {
+                       cancelledAlready = event.defaultPrevented;
+                       event.preventDefault();
+               });
+
+               assert.notOk(Wicket.Event.triggerSubmit(form), 'the Ajax 
request is stopped');
+               assert.notOk(cancelledAlready, 'not cancelled while the 
handlers on the form run');
+       });
+
+       test('a capturing handler stopping the propagation before the form lets 
the Ajax request go on', assert => {
+               const form = createTestForm();
+               const submissions = recordSubmissions(form);
+               let event;
+               const stop = function (e) {
+                       event = e;
+                       e.stopPropagation();
+               };
+               document.addEventListener('submit', stop, true);
+               try {
+                       assert.ok(Wicket.Event.triggerSubmit(form), 'the Ajax 
request may go on');
+               } finally {
+                       document.removeEventListener('submit', stop, true);
+               }
+               assert.ok(event.defaultPrevented, 'the event is cancelled');
+               assert.equal(submissions.length, 0, 'the browser does not 
submit the form');
+       });
+
+       test('a capturing handler cancelling the event and stopping the 
propagation stops the Ajax request', assert => {
+               const form = createTestForm();
+               const stop = function (e) {
+                       e.preventDefault();
+                       e.stopPropagation();
+               };
+               document.addEventListener('submit', stop, true);
+               try {
+                       assert.notOk(Wicket.Event.triggerSubmit(form));
+               } finally {
+                       document.removeEventListener('submit', stop, true);
+               }
+       });
+
+       test('a form with a control named submit is submitted by the form\'s 
own method', assert => {
+               const form = createTestForm();
+               const control = document.createElement('input');
+               control.name = 'submit';
+               form.appendChild(control);
+               const submissions = recordSubmissions(form);
+
+               // as Form#getJsForListenerUrl() submits it
+               if (Wicket.Event.triggerSubmit(form)) {
+                       HTMLFormElement.prototype.submit.call(form);
+               }
+
+               assert.equal(submissions.length, 1, 'the form is submitted 
once');
+       });
+
+       test('a handler stopping the immediate propagation without cancelling 
lets the Ajax request go on', assert => {
+               const form = createTestForm();
+               const submissions = recordSubmissions(form);
+               let event;
+               form.addEventListener('submit', function (e) {
+                       event = e;
+                       e.stopImmediatePropagation();
+               });
+
+               assert.ok(Wicket.Event.triggerSubmit(form), 'the Ajax request 
may go on');
+               assert.ok(event.defaultPrevented, 'the event is cancelled');
+               assert.equal(submissions.length, 0, 'the browser does not 
submit the form');
+       });
+
+       test('a handler cancelling the event and stopping the immediate 
propagation stops the Ajax request', assert => {
+               const form = createTestForm();
+               form.addEventListener('submit', function (event) {
+                       event.preventDefault();
+                       event.stopImmediatePropagation();
+               });
+
+               assert.notOk(Wicket.Event.triggerSubmit(form));
+       });
+
+       test('a form in a shadow root is not submitted', assert => {
+               const host = createTestElement('triggerSubmitShadowHost');
+               const form = createTestForm(host.attachShadow({ mode: 'open' 
}));
+               const submissions = recordSubmissions(form);
+               const events = recordSubmitEvents(form);
+
+               assert.ok(Wicket.Event.triggerSubmit(form), 'the Ajax request 
may go on');
+               assert.ok(events[0].defaultPrevented, 'the event is cancelled');
+               assert.equal(submissions.length, 0, 'the browser does not 
submit the form');
+       });
+
+       test('a submit triggered from a submit handler is not cancelled by the 
outer one', assert => {
+               const outer = createTestForm();
+               const inner = createTestForm();
+               let innerResult;
+               outer.addEventListener('submit', function () {
+                       innerResult = Wicket.Event.triggerSubmit(inner);
+               });
+
+               assert.ok(Wicket.Event.triggerSubmit(outer), 'the outer Ajax 
request may go on');
+               assert.ok(innerResult, 'the inner Ajax request may go on');
+       });
+
        module('Wicket.Event.pubsub');
 
        test('specified topic', assert => {
diff --git 
a/wicket-examples/src/test/java/org/apache/wicket/examples/SubmitEventSeleniumTest.java
 
b/wicket-examples/src/test/java/org/apache/wicket/examples/SubmitEventSeleniumTest.java
new file mode 100644
index 0000000000..4c20cb477e
--- /dev/null
+++ 
b/wicket-examples/src/test/java/org/apache/wicket/examples/SubmitEventSeleniumTest.java
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.wicket.examples;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.net.URI;
+import java.time.Duration;
+
+import org.apache.wicket.examples.AjaxEngineSelector.Engine;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.openqa.selenium.By;
+import org.openqa.selenium.Capabilities;
+import org.openqa.selenium.JavascriptExecutor;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.chrome.ChromeDriver;
+import org.openqa.selenium.chrome.ChromeOptions;
+import org.openqa.selenium.firefox.FirefoxDriver;
+import org.openqa.selenium.firefox.FirefoxOptions;
+import org.openqa.selenium.remote.RemoteWebDriver;
+import org.openqa.selenium.support.ui.ExpectedConditions;
+import org.openqa.selenium.support.ui.Select;
+import org.openqa.selenium.support.ui.WebDriverWait;
+
+/**
+ * Checks that the forms which fire a submit event from a script are submitted 
exactly once, in a
+ * real browser and with either Ajax engine. Browsers differ on such an event: 
Firefox submits the
+ * form for it unless it is cancelled, Chrome never does.
+ * <p>
+ * Needs a browser, so it only runs with {@code -Dwicket.selenium=true}. It 
drives a headless
+ * Chrome by default; {@code -Dwicket.selenium.browser=firefox} picks Firefox. 
With
+ * {@code -Dwicket.selenium.remote=<url>} the browser runs on that Selenium 
grid instead, and
+ * {@code -Dwicket.selenium.host=<host>} names the host under which the grid 
reaches this machine.
+ */
+@EnabledIfSystemProperty(named = "wicket.selenium", matches = "true")
+class SubmitEventSeleniumTest extends JettyTestCaseDecorator
+{
+       /**
+        * Counts the {@code formdata} events of the given form in the session 
storage, so the count
+        * survives a page load. The Ajax engines build their form data without 
the form, so only a
+        * submission by the browser fires one.
+        */
+       private static final String RECORD_SUBMISSIONS = """
+               sessionStorage.setItem('submissions', '0');
+               arguments[0].addEventListener('formdata', function () {
+                       sessionStorage.setItem('submissions',
+                               
String(Number(sessionStorage.getItem('submissions')) + 1));
+               });
+               """;
+
+       private WebDriver driver;
+
+       private WebDriverWait wait;
+
+       @Override
+       @BeforeEach
+       public void before() throws Exception
+       {
+               super.before();
+
+               driver = createDriver();
+               wait = new WebDriverWait(driver, Duration.ofSeconds(10), 
Duration.ofMillis(20));
+       }
+
+       private static WebDriver createDriver() throws Exception
+       {
+               boolean firefox = 
"firefox".equals(System.getProperty("wicket.selenium.browser"));
+               String remote = System.getProperty("wicket.selenium.remote");
+
+               Capabilities options;
+               if (firefox)
+               {
+                       options = new 
FirefoxOptions().addArguments("-headless");
+               }
+               else
+               {
+                       options = new 
ChromeOptions().addArguments("--headless=new");
+               }
+
+               if (remote != null)
+               {
+                       return new RemoteWebDriver(URI.create(remote).toURL(), 
options);
+               }
+               return firefox ? new FirefoxDriver((FirefoxOptions)options)
+                       : new ChromeDriver((ChromeOptions)options);
+       }
+
+       @Override
+       @AfterEach
+       public void after() throws Exception
+       {
+               if (driver != null)
+               {
+                       driver.quit();
+               }
+               super.after();
+       }
+
+       /**
+        * The Ajax button of the upload example triggers a submit event to 
start the progress bar.
+        */
+       @ParameterizedTest
+       @EnumSource(Engine.class)
+       void anAjaxButtonTriggeringTheSubmitEventSubmitsOnlyThroughAjax(Engine 
engine)
+       {
+               open(engine, "ajax/upload");
+
+               WebElement button = 
driver.findElements(By.cssSelector("input[value='Ajax Submit']"))
+                       .get(0);
+               js().executeScript(RECORD_SUBMISSIONS, 
button.findElement(By.xpath("./ancestor::form")));
+               button.click();
+
+               wait.until(d -> submissions() > 0 ||
+                       d.getPageSource().contains("This request was processed 
using AJAX"));
+               assertEquals(0, submissions(), "the browser submitted the form 
besides the Ajax request");
+       }
+
+       /**
+        * A {@code FormComponentUpdatingBehavior} submits its form through
+        * {@code Form#getJsForListenerUrl()}.
+        */
+       @ParameterizedTest
+       @EnumSource(Engine.class)
+       void aFormComponentUpdatingBehaviorSubmitsTheForm(Engine engine)
+       {
+               open(engine, "forminput");
+
+               WebElement locale = 
driver.findElement(By.cssSelector("select[name$='localeSelect']"));
+               int other = index(new Select(locale).getFirstSelectedOption()) 
== 0 ? 1 : 0;
+               new Select(locale).selectByIndex(other);
+
+               wait.until(ExpectedConditions.stalenessOf(locale));
+               WebElement reloaded = 
driver.findElement(By.cssSelector("select[name$='localeSelect']"));
+               assertEquals(other, index(new 
Select(reloaded).getFirstSelectedOption()),
+                       "the chosen locale is in effect");
+       }
+
+       private void open(Engine engine, String path)
+       {
+               String host = System.getProperty("wicket.selenium.host", 
"localhost");
+               driver.get(String.format("http://%s:%d/wicket-examples/%s";, 
host, localPort, path));
+               if (isJQueryLoaded() != (engine == Engine.JQUERY))
+               {
+                       WebElement toggle = 
driver.findElement(By.partialLinkText("switch to"));
+                       toggle.click();
+                       wait.until(ExpectedConditions.stalenessOf(toggle));
+               }
+               assertEquals(engine == Engine.JQUERY, isJQueryLoaded(), "the 
Ajax engine");
+               assertTrue((Boolean)js().executeScript("return typeof 
Wicket.Event === 'object';"),
+                       "Wicket's JavaScript is loaded");
+       }
+
+       private boolean isJQueryLoaded()
+       {
+               return (Boolean)js().executeScript("return typeof window.jQuery 
=== 'function';");
+       }
+
+       private long submissions()
+       {
+               return (Long)js().executeScript(
+                       "return 
Number(sessionStorage.getItem('submissions'));");
+       }
+
+       private static int index(WebElement option)
+       {
+               return Integer.parseInt(option.getDomProperty("index"));
+       }
+
+       private JavascriptExecutor js()
+       {
+               return (JavascriptExecutor)driver;
+       }
+}

Reply via email to