Bug 1186932 - Implement support for form submission of a picked directory - part 3 - tests + fixes, r=smaug
authorAndrea Marchesini <amarchesini@mozilla.com>
Thu, 14 Jul 2016 09:02:30 +0200
changeset 330069 e3e3c703163d06dcaf97b5db6448a19eb7efb736
parent 330068 8105df56b23209b8cb3389dfe483214472e0bb3f
child 330070 57a8705babf19b7b58e706964c32c87bd9815d6f
push id9858
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 14:37:10 +0000
treeherdermozilla-aurora@203106ef6cb6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1186932
milestone50.0a1
Bug 1186932 - Implement support for form submission of a picked directory - part 3 - tests + fixes, r=smaug
dom/filesystem/compat/tests/mochitest.ini
dom/filesystem/compat/tests/test_formSubmission.html
dom/html/HTMLInputElement.cpp
dom/html/HTMLInputElement.h
--- a/dom/filesystem/compat/tests/mochitest.ini
+++ b/dom/filesystem/compat/tests/mochitest.ini
@@ -1,6 +1,8 @@
 [DEFAULT]
 support-files =
   script_entries.js
+  !/dom/html/test/form_submit_server.sjs
 
 [test_basic.html]
 [test_no_dnd.html]
+[test_formSubmission.html]
new file mode 100644
--- /dev/null
+++ b/dom/filesystem/compat/tests/test_formSubmission.html
@@ -0,0 +1,265 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for Directory form submission</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+  <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
+</head>
+<body onload="return next();">
+
+<iframe name="target_iframe" id="target_iframe"></iframe>
+
+<form action="../../../html/test/form_submit_server.sjs" target="target_iframe" id="form"
+      method="POST" enctype="multipart/form-data">
+</form>
+
+<script class="testbody" type="text/javascript;version=1.8">
+
+var form;
+var iframe;
+var input;
+var xhr;
+
+function setup_tests() {
+  form = document.getElementById("form");
+
+  iframe = document.getElementById("target_iframe");
+  iframe.onload = function() {
+    info("Frame loaded!");
+    next();
+  }
+
+  SpecialPowers.pushPrefEnv({"set": [["dom.webkitBlink.dirPicker.enabled", true],
+                                     ["dom.webkitBlink.filesystem.enabled", true]]}, next);
+}
+
+function populate_entries(webkitDirectory) {
+  if (input) {
+    form.removeChild(input);
+  }
+
+  input = document.createElement('input');
+  input.setAttribute('id', 'input');
+  input.setAttribute('type', 'file');
+  input.setAttribute('name', 'input');
+
+  if (webkitDirectory) {
+    input.setAttribute('webkitdirectory', 'true');
+  }
+
+  form.appendChild(input);
+
+  var url = SimpleTest.getTestFileURL("script_entries.js");
+  var script = SpecialPowers.loadChromeScript(url);
+
+  function onOpened(message) {
+    input.addEventListener("change", function onChange() {
+      input.removeEventListener("change", onChange);
+      next();
+    });
+
+    SpecialPowers.wrap(input).mozSetDndFilesAndDirectories([message.data[0]]);
+    script.destroy();
+  }
+
+  script.addMessageListener("entries.opened", onOpened);
+  script.sendAsyncMessage("entries.open");
+}
+
+function setup_plain() {
+  info("Preparing for a plain text submission...");
+  form.action = "../../../html/test/form_submit_server.sjs?plain";
+  form.method = "POST";
+  form.enctype = "text/plain";
+  form.submit();
+}
+
+function test_plain() {
+  var content = iframe.contentDocument.documentElement.textContent;
+  var submission = JSON.parse(content);
+  input.getFilesAndDirectories().then(function(array) {
+    is(submission, array.map(function(v) {
+       return "input=" + v.name + "\r\n";
+    }).join(""), "Data match");
+
+    next();
+  });
+}
+
+function setup_urlencoded() {
+  info("Preparing for a urlencoded submission...");
+  form.action = "../../../html/test/form_submit_server.sjs?url";
+  form.method = "POST";
+  form.enctype = "application/x-www-form-urlencoded";
+  form.submit();
+}
+
+function setup_urlencoded_get() {
+  info("Preparing for a urlencoded+GET submission...");
+  form.action = "../../../html/test/form_submit_server.sjs?xxyy";
+  form.method = "GET";
+  form.enctype = "";
+  form.submit();
+}
+
+function setup_urlencoded_empty() {
+  info("Preparing for a urlencoded+default values submission...");
+  form.action = "../../../html/test/form_submit_server.sjs";
+  form.method = "";
+  form.enctype = "";
+  form.submit();
+}
+
+function test_urlencoded() {
+  var content = iframe.contentDocument.documentElement.textContent;
+  var submission = JSON.parse(content);
+  input.getFilesAndDirectories().then(function(array) {
+    is(submission, array.map(function(v) {
+       return "input=" + v.name;
+    }).join("&"), "Data match");
+
+    next();
+  });
+}
+
+function setup_formData() {
+  info("Preparing for a fromData submission...");
+
+  xhr = new XMLHttpRequest();
+  xhr.onload = next;
+  xhr.open("POST", "../../../html/test/form_submit_server.sjs");
+  xhr.send(new FormData(form));
+}
+
+function test_multipart() {
+  var submission = JSON.parse(xhr.responseText);
+  input.getFilesAndDirectories().then(function(array) {
+    is(submission.length, array.length, "Same length");
+
+    for (var i = 0; i < array.length; ++i) {
+      if (array[i] instanceof Directory) {
+        is(submission[i].headers["Content-Disposition"],
+           "form-data; name=\"input\"; filename=\"/" + array[i].name + "\"",
+           "Correct Content-Disposition");
+        is(submission[i].headers["Content-Type"], "application/octet-stream",
+           "Correct Content-Type");
+        is(submission[i].body, "", "Correct body");
+      } else {
+        ok(array[i] instanceof File);
+        is(submission[i].headers["Content-Disposition"],
+           "form-data; name=\"input\"; filename=\"" + array[i].name + "\"",
+           "Correct Content-Disposition");
+        is(submission[i].headers["Content-Type"], array[i].type,
+           "Correct Content-Type");
+        is(submission[i].body, "", "Correct body");
+      }
+    }
+    next();
+  });
+}
+
+function test_webkit_plain() {
+  var content = iframe.contentDocument.documentElement.textContent;
+  var submission = JSON.parse(content);
+
+  input.getFiles(true).then(function(array) {
+    is(submission, array.map(function(v) {
+       return "input=" + v.name + "\r\n";
+    }).join(""), "Data match");
+
+    next();
+  });
+}
+
+function test_webkit_urlencoded() {
+  var content = iframe.contentDocument.documentElement.textContent;
+  var submission = JSON.parse(content);
+  input.getFiles(true).then(function(array) {
+    is(submission, array.map(function(v) {
+       return "input=" + v.name;
+    }).join("&"), "Data match");
+
+    next();
+  });
+}
+
+function test_webkit_multipart() {
+  var submission = JSON.parse(xhr.responseText);
+  input.getFiles(true).then(function(array) {
+    is(submission.length, array.length, "Same length");
+
+    for (var i = 0; i < array.length; ++i) {
+      if (array[i] instanceof Directory) {
+        is(submission[i].headers["Content-Disposition"],
+           "form-data; name=\"input\"; filename=\"/" + array[i].name + "\"",
+           "Correct Content-Disposition");
+        is(submission[i].headers["Content-Type"], "application/octet-stream",
+           "Correct Content-Type");
+        is(submission[i].body, "", "Correct body");
+      } else {
+        ok(array[i] instanceof File);
+        is(submission[i].headers["Content-Disposition"],
+           "form-data; name=\"input\"; filename=\"" + array[i].webkitRelativePath + "\"",
+           "Correct Content-Disposition");
+        is(submission[i].headers["Content-Type"], array[i].type,
+           "Correct Content-Type");
+        is(submission[i].body, "", "Correct body");
+      }
+    }
+    next();
+  });
+}
+
+var tests = [
+  setup_tests,
+  function() { populate_entries(false); },
+
+  setup_plain,
+  test_plain,
+
+  setup_urlencoded,
+  test_urlencoded,
+
+  setup_urlencoded_get,
+  test_urlencoded,
+
+  setup_urlencoded_empty,
+  test_urlencoded,
+
+  setup_formData,
+  test_multipart,
+
+  function() { populate_entries(true); },
+
+  setup_plain,
+  test_webkit_plain,
+
+  setup_urlencoded,
+  test_webkit_urlencoded,
+
+  setup_urlencoded_get,
+  test_webkit_urlencoded,
+
+  setup_urlencoded_empty,
+  test_webkit_urlencoded,
+
+  setup_formData,
+  test_webkit_multipart,
+];
+
+function next() {
+  if (!tests.length) {
+    SimpleTest.finish();
+    return;
+  }
+
+  var test = tests.shift();
+  test();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</body>
+</html>
--- a/dom/html/HTMLInputElement.cpp
+++ b/dom/html/HTMLInputElement.cpp
@@ -619,16 +619,20 @@ private:
 
   Mutex mMutex;
 
   // This variable is protected by mutex.
   bool mCanceled;
 };
 
 // An helper class for the dispatching of the 'change' event.
+// This class is used when the FilePicker finished its task (or when files and
+// directories are set by some chrome/test only method).
+// The task of this class is to postpone the dispatching of 'change' and 'input'
+// events at the end of the exploration of the directories.
 class DispatchChangeEventCallback final : public GetFilesCallback
 {
 public:
   explicit DispatchChangeEventCallback(HTMLInputElement* aInputElement)
     : mInputElement(aInputElement)
   {
     MOZ_ASSERT(aInputElement);
   }
@@ -663,42 +667,16 @@ public:
 
     return rv;
   }
 
 private:
   RefPtr<HTMLInputElement> mInputElement;
 };
 
-// This callback is used for postponing the calling of SetFilesOrDirectories
-// when the exploration of the directory is completed.
-class AfterSetFilesOrDirectoriesCallback : public GetFilesCallback
-{
-public:
-  AfterSetFilesOrDirectoriesCallback(HTMLInputElement* aInputElement,
-                                     bool aSetValueChanged)
-    : mInputElement(aInputElement)
-    , mSetValueChanged(aSetValueChanged)
-  {
-    MOZ_ASSERT(aInputElement);
-  }
-
-  void
-  Callback(nsresult aStatus, const Sequence<RefPtr<File>>& aFiles) override
-  {
-    if (NS_SUCCEEDED(aStatus)) {
-      mInputElement->AfterSetFilesOrDirectoriesInternal(mSetValueChanged);
-    }
-  }
-
-private:
-  RefPtr<HTMLInputElement> mInputElement;
-  bool mSetValueChanged;
-};
-
 class HTMLInputElementState final : public nsISupports
 {
   public:
     NS_DECLARE_STATIC_IID_ACCESSOR(NS_INPUT_ELEMENT_STATE_IID)
     NS_DECL_ISUPPORTS
 
     bool IsCheckedSet()
     {
@@ -3309,42 +3287,48 @@ HTMLInputElement::SetFiles(nsIDOMFileLis
       OwningFileOrDirectory* element = mFilesOrDirectories.AppendElement();
       element->SetAsFile() = files->Item(i);
     }
   }
 
   AfterSetFilesOrDirectories(aSetValueChanged);
 }
 
+// This method is used for testing only.
 void
 HTMLInputElement::MozSetDndFilesAndDirectories(const nsTArray<OwningFileOrDirectory>& aFilesOrDirectories)
 {
   if (Preferences::GetBool("dom.webkitBlink.filesystem.enabled", false)) {
     UpdateEntries(aFilesOrDirectories);
   }
 
   SetFilesOrDirectories(aFilesOrDirectories, true);
+
+  RefPtr<DispatchChangeEventCallback> dispatchChangeEventCallback =
+    new DispatchChangeEventCallback(this);
+
+  if (Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) &&
+      HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory)) {
+    ErrorResult rv;
+    GetFilesHelper* helper = GetOrCreateGetFilesHelper(true /* recursionFlag */,
+                                                       rv);
+    if (NS_WARN_IF(rv.Failed())) {
+      rv.SuppressException();
+      return;
+    }
+
+    helper->AddCallback(dispatchChangeEventCallback);
+  } else {
+    dispatchChangeEventCallback->DispatchEvents();
+  }
 }
 
 void
 HTMLInputElement::AfterSetFilesOrDirectories(bool aSetValueChanged)
 {
-  if (Preferences::GetBool("dom.webkitBlink.dirPicker.enabled", false) &&
-      HasAttr(kNameSpaceID_None, nsGkAtoms::webkitdirectory)) {
-    // This will call AfterSetFilesOrDirectoriesInternal eventually.
-    ExploreDirectoryRecursively(aSetValueChanged);
-    return;
-  }
-
-  AfterSetFilesOrDirectoriesInternal(aSetValueChanged);
-}
-
-void
-HTMLInputElement::AfterSetFilesOrDirectoriesInternal(bool aSetValueChanged)
-{
   // No need to flush here, if there's no frame at this point we
   // don't need to force creation of one just to tell it about this
   // new value.  We just want the display to update as needed.
   nsIFormControlFrame* formControlFrame = GetFormControlFrame(false);
   if (formControlFrame) {
     nsAutoString readableValue;
     GetDisplayFileName(readableValue);
     formControlFrame->SetFormProperty(nsGkAtoms::value, readableValue);
@@ -8529,32 +8513,16 @@ HTMLInputElement::GetOrCreateGetFilesHel
       return nullptr;
     }
   }
 
   return mGetFilesNonRecursiveHelper;
 }
 
 void
-HTMLInputElement::ExploreDirectoryRecursively(bool aSetValueChanged)
-{
-  ErrorResult rv;
-  GetFilesHelper* helper = GetOrCreateGetFilesHelper(true /* recursionFlag */,
-                                                     rv);
-  if (NS_WARN_IF(rv.Failed())) {
-    AfterSetFilesOrDirectoriesInternal(aSetValueChanged);
-    return;
-  }
-
-  RefPtr<AfterSetFilesOrDirectoriesCallback> callback =
-    new AfterSetFilesOrDirectoriesCallback(this, aSetValueChanged);
-  helper->AddCallback(callback);
-}
-
-void
 HTMLInputElement::UpdateEntries(const nsTArray<OwningFileOrDirectory>& aFilesOrDirectories)
 {
   mEntries.Clear();
 
   nsCOMPtr<nsIGlobalObject> global = OwnerDoc()->GetScopeObject();
   MOZ_ASSERT(global);
 
   RefPtr<DOMFileSystem> fs = DOMFileSystem::Create(global);
--- a/dom/html/HTMLInputElement.h
+++ b/dom/html/HTMLInputElement.h
@@ -238,16 +238,18 @@ public:
   {
     return mFilesOrDirectories;
   }
 
   void SetFilesOrDirectories(const nsTArray<OwningFileOrDirectory>& aFilesOrDirectories,
                              bool aSetValueChanged);
   void SetFiles(nsIDOMFileList* aFiles, bool aSetValueChanged);
 
+  // This method is used for test only. Onces the data is set, a 'change' event
+  // is dispatched.
   void MozSetDndFilesAndDirectories(const nsTArray<OwningFileOrDirectory>& aSequence);
 
   // Called when a nsIFilePicker or a nsIColorPicker terminate.
   void PickerClosed();
 
   void SetCheckedChangedInternal(bool aCheckedChanged);
   bool GetCheckedChanged() const {
     return mCheckedChanged;
@@ -966,17 +968,16 @@ protected:
 
   void UpdateEntries(const nsTArray<OwningFileOrDirectory>& aFilesOrDirectories);
 
   /**
    * Called after calling one of the SetFilesOrDirectories() functions.
    * This method can explore the directory recursively if needed.
    */
   void AfterSetFilesOrDirectories(bool aSetValueChanged);
-  void AfterSetFilesOrDirectoriesInternal(bool aSetValueChanged);
 
   /**
    * Recursively explore the directory and populate mFileOrDirectories correctly
    * for webkitdirectory.
    */
   void ExploreDirectoryRecursively(bool aSetValuechanged);
 
   /**