Make the submission from default action of a submit button also deferred.

The submission from requestSubmit() call is non-deferred due to
mDeferSubmission
being cleared in HTMLFormElement::PostHandleEvent when handling a submit
event. The
mDeferSubmission clearing is also to make the submission from the default
action
of a submit button non-deferred.

This patch makes the submission from default action of a submit button
deferred
which could fix this bug, and also simplify the logic a bit.

Since gecko would also trigger submission for untrusted submit event,
see
bug 1370630, we need to handle that case specially.

Differential Revision: https://phabricator.services.mozilla.com/D170941

bugzilla-url: https://bugzilla.mozilla.org/show_bug.cgi?id=1653625
gecko-commit: aec89f8bbc3c570aefe80750183bb41fea3924d7
gecko-reviewers: edgar
diff --git a/html/semantics/forms/form-submission-0/form-double-submit-requestsubmit.html b/html/semantics/forms/form-submission-0/form-double-submit-requestsubmit.html
new file mode 100644
index 0000000..b402878
--- /dev/null
+++ b/html/semantics/forms/form-submission-0/form-double-submit-requestsubmit.html
@@ -0,0 +1,142 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<link rel="help"
+  href="https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm">
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="./resources/targetted-form.js"></script>
+
+<!-- The onclick requestSubmit() should get superseded by the default
+     action submit, which isn't preventDefaulted by onclick here.
+     This is per the Form Submission Algorithm [1], which
+     says that new planned navigations replace old planned navigations.
+     [1] https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#planned-navigation
+  -->
+
+<body>
+  <script>
+    function runTest({ submitterType, preventDefaultSubmitButton, preventDefaultRequestSubmit, passSubmitter, testName }) {
+      if (preventDefaultRequestSubmit && preventDefaultSubmitButton) {
+        // In this case, no submit action will take place.
+        return;
+      }
+
+      promise_test(async () => {
+        const form = populateForm(`<input name=n1 value=v1><${submitterType} type=submit name=n2 value=v2>${submitterType == 'button' ? '</button>' : ''}`);
+        const input = form.elements[0];
+        const submitter = form.elements[1];
+        submitter.addEventListener('click', e => {
+          form.addEventListener('submit', e => {
+            submitter.value = 'v3';
+            if (preventDefaultRequestSubmit) {
+              e.preventDefault();
+            }
+          }, { once: true });
+
+          form.requestSubmit(passSubmitter ? submitter : null);
+          input.value = 'v2';
+
+          form.addEventListener('submit', e => {
+            submitter.value = 'v4';
+            if (preventDefaultSubmitButton) {
+              e.preventDefault();
+            }
+          }, { once: true });
+        });
+
+        let formDataInEvent;
+        form.addEventListener('formdata', e => {
+          formDataInEvent = e.formData;
+        });
+
+        submitter.click();
+        assert_equals(formDataInEvent.get('n1'), preventDefaultSubmitButton ? 'v1' : 'v2');
+        if (preventDefaultSubmitButton && !passSubmitter) {
+          assert_false(formDataInEvent.has('n2'));
+        } else {
+          assert_equals(formDataInEvent.get('n2'),
+            preventDefaultSubmitButton && passSubmitter ? 'v3' : 'v4')
+        }
+
+        let iframe = form.previousSibling;
+        await loadPromise(iframe);
+        assert_equals(getParamValue(iframe, 'n1'), preventDefaultSubmitButton ? 'v1' : 'v2');
+        if (preventDefaultSubmitButton && !passSubmitter) {
+          assert_equals(getParamValue(iframe, 'n2'), null);
+        } else {
+          assert_equals(getParamValue(iframe, 'n2'),
+            preventDefaultSubmitButton && passSubmitter ? 'v3' : 'v4');
+        }
+      }, testName);
+    }
+
+    function runTest2({ submitterType, callRequestSubmit, callSubmit, preventDefault, passSubmitter, testName }) {
+      if (!callSubmit && preventDefault) {
+        // Without callSubmit, preventDefault will cause the form to not get
+        // submitted.
+        return;
+      }
+
+      promise_test(async () => {
+        const form = populateForm(`<input name=n1 value=v1><${submitterType} type=submit name=n2 value=v3>${submitterType == 'button' ? '</button>' : ''}`);
+        const input = form.elements[0];
+        const submitter = form.elements[1];
+
+        form.addEventListener('submit', e => {
+          if (callRequestSubmit) {
+            form.requestSubmit(passSubmitter ? submitter : null);
+            input.value = 'v2';
+          }
+          if (callSubmit) {
+            form.submit();
+          }
+          if (preventDefault) {
+            e.preventDefault();
+          }
+        });
+
+        form.requestSubmit(passSubmitter ? submitter : null);
+        let iframe = form.previousSibling;
+        await loadPromise(iframe);
+
+        assert_equals(getParamValue(iframe, 'n1'), callRequestSubmit ? 'v2' : 'v1');
+        if (callSubmit || !passSubmitter) {
+          assert_equals(getParamValue(iframe, 'n2'), null);
+        } else {
+          assert_equals(getParamValue(iframe, 'n2'), 'v3')
+        }
+      }, testName);
+    }
+
+    function callWithArgs(test, argsLeft, args) {
+      if (argsLeft.length == 0) {
+        args.testName = 'test ' + test.name + ' with ' + Object.entries(args).map(([key, value]) => `${key}: ${value}`).join(', ');
+        test(args);
+        return;
+      }
+
+      let [name, values] = argsLeft[0];
+      for (let value of values) {
+        callWithArgs(test, argsLeft.slice(1), { ...args, [name]: value })
+      }
+    }
+
+    let args = {
+      submitterType: ['input', 'button'],
+      preventDefaultRequestSubmit: [true, false],
+      preventDefaultSubmitButton: [true, false],
+      passSubmitter: [true, false],
+    };
+    callWithArgs(runTest, Object.entries(args), {});
+
+    args = {
+      submitterType: ['input', 'button'],
+      callRequestSubmit: [true, false],
+      callSubmit: [true, false],
+      preventDefault: [true, false],
+      passSubmitter: [true, false],
+    };
+    callWithArgs(runTest2, Object.entries(args), {});
+  </script>
+</body>