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¶m2=val%20ue';Wicket.Event.fire(f,
'submit');",
+ assertEquals("var f =
document.getElementById('formpost1');f.action='path?param1¶m2=val%20ue';if
(Wicket.Event.triggerSubmit(f)) { HTMLFormElement.prototype.submit.call(f); }",
page.postForm.getJsForListenerUrl("path?param1¶m2=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¶m2=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;
+ }
+}