Bug 1251809 - Add input[type=file] tooltip support for e10s. r?enn,ehsan draft
authorJared Wein <jwein@mozilla.com>
Mon, 14 Mar 2016 22:49:42 -0400
changeset 340272 806a738547cdd36583f869a3a480bda33a087d44
parent 337312 fd095cbb65ad0d4e20c10f7a1c1c3e7dcf3abdab
child 516156 fa009e605ee186117ad2c6374a96f8a7e5ef75cb
push id12926
push userjwein@mozilla.com
push dateTue, 15 Mar 2016 02:50:43 +0000
reviewersenn, ehsan
bugs1251809
milestone47.0a1
Bug 1251809 - Add input[type=file] tooltip support for e10s. r?enn,ehsan This patch adds support for tooltips on input[type=file] in e10s. I've updated the automated test to work in both non-e10s and e10s, but was unable to add test cases for multiple files being selected due to an issue with MockFilePicker where it only returns the first file from the returnList. Testing for multiple files will need to be done manually. Also, popopup.xml includes support for adding "And x more..." at the end of the tooltip if the selected number of files is more than our truncation amount. I was unable to add that to the remote implementation since I didn't see a way to use PluralForm-syntax strings from C++. I also didn' want to add a new string to cover this since we want to uplift these patches to 46. MozReview-Commit-ID: B3524S0drrP
embedding/browser/nsDocShellTreeOwner.cpp
toolkit/content/tests/browser/browser.ini
toolkit/content/tests/browser/browser_input_file_tooltips.js
toolkit/content/widgets/popup.xml
--- a/embedding/browser/nsDocShellTreeOwner.cpp
+++ b/embedding/browser/nsDocShellTreeOwner.cpp
@@ -30,29 +30,31 @@
 #include "nsIDOMNodeList.h"
 #include "nsIDOMDocument.h"
 #include "nsIDOMDocumentType.h"
 #include "nsIDOMElement.h"
 #include "Link.h"
 #include "mozilla/dom/Element.h"
 #include "mozilla/dom/SVGTitleElement.h"
 #include "nsIDOMEvent.h"
+#include "nsIDOMFileList.h"
 #include "nsIDOMMouseEvent.h"
 #include "nsIFormControl.h"
 #include "nsIDOMHTMLInputElement.h"
 #include "nsIDOMHTMLTextAreaElement.h"
 #include "nsIDOMHTMLHtmlElement.h"
 #include "nsIDOMHTMLAppletElement.h"
 #include "nsIDOMHTMLObjectElement.h"
 #include "nsIDOMHTMLEmbedElement.h"
 #include "nsIDOMHTMLDocument.h"
 #include "nsIImageLoadingContent.h"
 #include "nsIWebNavigation.h"
 #include "nsIDOMHTMLElement.h"
 #include "nsIPresShell.h"
+#include "nsIStringBundle.h"
 #include "nsPIDOMWindow.h"
 #include "nsPIWindowRoot.h"
 #include "nsIDOMWindowCollection.h"
 #include "nsIWindowWatcher.h"
 #include "nsPIWindowWatcher.h"
 #include "nsIPrompt.h"
 #include "nsITabParent.h"
 #include "nsRect.h"
@@ -63,16 +65,18 @@
 #include "nsPresContext.h"
 #include "nsViewManager.h"
 #include "nsView.h"
 #include "nsIDOMDragEvent.h"
 #include "nsIConstraintValidation.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/EventListenerManager.h"
 #include "mozilla/dom/Event.h" // for nsIDOMEvent::InternalDOMEvent()
+#include "mozilla/dom/File.h" // for input type=file
+#include "mozilla/dom/FileList.h" // for input type=file
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
 // A helper routine that navigates the tricky path from a |nsWebBrowser| to
 // a |EventTarget| via the window root and chrome event handler.
 static nsresult
 GetDOMEventTarget(nsWebBrowser* aInBrowser, EventTarget** aTarget)
@@ -1142,18 +1146,72 @@ DefaultTooltipTextProvider::GetNodeText(
     if (currElement) {
       nsCOMPtr<nsIContent> content(do_QueryInterface(currElement));
       if (content) {
         if (!content->IsAnyOfXULElements(mTag_dialog,
                                          mTag_dialogheader,
                                          mTag_window)) {
           // first try the normal title attribute...
           if (!content->IsSVGElement()) {
-            currElement->GetAttribute(NS_LITERAL_STRING("title"), outText);
-            if (outText.Length()) {
+            nsCOMPtr<nsIAtom> inputAtom = do_GetAtom("input");
+            nsString typeText;
+            bool hasAttr = false;
+            // If the element is an <input type="file"> without a title,
+            // we should show the current file selection.
+            if (content->IsHTMLElement(inputAtom) &&
+                NS_SUCCEEDED(currElement->GetAttribute(NS_LITERAL_STRING("type"), typeText)) &&
+                typeText.EqualsLiteral("file") &&
+                NS_SUCCEEDED(currElement->HasAttribute(NS_LITERAL_STRING("title"), &hasAttr)) &&
+                !hasAttr) {
+              found = true;
+              nsCOMPtr<nsIDOMFileList> fileList;
+              nsCOMPtr<nsIDOMHTMLInputElement> currInputElement(do_QueryInterface(currElement));
+              currInputElement->GetFiles(getter_AddRefs(fileList));
+
+              nsCOMPtr<nsIStringBundleService> bundleService =
+                mozilla::services::GetStringBundleService();
+              if (!bundleService) {
+                return NS_ERROR_FAILURE;
+              }
+
+              nsCOMPtr<nsIStringBundle> bundle;
+              nsresult rv = bundleService->CreateBundle("chrome://global/locale/layout/HtmlForm.properties",
+                                                        getter_AddRefs(bundle));
+              NS_ENSURE_SUCCESS(rv, rv);
+              uint32_t listLength = 0;
+              rv = fileList->GetLength(&listLength);
+              NS_ENSURE_SUCCESS(rv, rv);
+              if (listLength == 0) {
+                currElement->HasAttribute(NS_LITERAL_STRING("multiple"), &hasAttr);
+                if (hasAttr) {
+                  rv = bundle->GetStringFromName(MOZ_UTF16("NoFilesSelected"),
+                                                 getter_Copies(outText));
+                } else {
+                  rv = bundle->GetStringFromName(MOZ_UTF16("NoFileSelected"),
+                                                 getter_Copies(outText));
+                }
+                NS_ENSURE_SUCCESS(rv, rv);
+              } else {
+                FileList* fl = static_cast<FileList*>(fileList.get());
+                fl->Item(0)->GetName(outText);
+
+                // For UX and performance (jank) reasons we cap the number of
+                // files that we list in the tooltip to 20 plus a "and xxx more"
+                // line, or to 21 if exactly 21 files were picked.
+                const uint32_t TRUNCATED_FILE_COUNT = 20;
+                uint32_t count = std::min(listLength, TRUNCATED_FILE_COUNT);
+                for (uint32_t i = 1; i < count; ++i) {
+                  nsString fileName;
+                  fl->Item(i)->GetName(fileName);
+                  outText.Append(NS_LITERAL_STRING("\n"));
+                  outText.Append(fileName);
+                }
+              }
+            } else if (NS_SUCCEEDED(currElement->GetAttribute(NS_LITERAL_STRING("title"), outText)) &&
+                       outText.Length()) {
               found = true;
             }
           }
           if (!found) {
             // ...ok, that didn't work, try it in the XLink namespace
             NS_NAMED_LITERAL_STRING(xlinkNS, "http://www.w3.org/1999/xlink");
             nsCOMPtr<mozilla::dom::Link> linkContent(
               do_QueryInterface(currElement));
--- a/toolkit/content/tests/browser/browser.ini
+++ b/toolkit/content/tests/browser/browser.ini
@@ -12,17 +12,16 @@ skip-if = buildapp == 'mulet' || e10s # 
 skip-if = e10s # Bug 1064580
 [browser_bug1198465.js]
 [browser_contentTitle.js]
 [browser_default_image_filename.js]
 [browser_f7_caret_browsing.js]
 skip-if = e10s
 [browser_findbar.js]
 [browser_input_file_tooltips.js]
-skip-if = e10s # Bug 1236991 - Update or remove tests that use fillInPageTooltip
 [browser_isSynthetic.js]
 support-files =
   empty.png
 [browser_keyevents_during_autoscrolling.js]
 [browser_save_resend_postdata.js]
 support-files =
   common/mockTransfer.js
   data/post_form_inner.sjs
--- a/toolkit/content/tests/browser/browser_input_file_tooltips.js
+++ b/toolkit/content/tests/browser/browser_input_file_tooltips.js
@@ -1,38 +1,110 @@
-function test()
-{
-  let data = [
-    { value: "/tmp", result: "tmp" },
-    { title: "foo", result: "foo" },
-    { result: "No file selected." },
-    { multiple: true, result: "No files selected." },
-    { required: true, result: "Please select a file." }
-  ];
+
+let tempFile;
+add_task(function* setup() {
+  yield new Promise(resolve => {
+    SpecialPowers.pushPrefEnv({"set": [["ui.tooltipDelay", 0]]}, resolve);
+  });
+  tempFile = createTempFile();
+  registerCleanupFunction(function() {
+    tempFile.remove(true);
+  });
+});
 
-  let doc = gBrowser.contentDocument;
-  let tooltip = document.getElementById("aHTMLTooltip");
+add_task(function* test_singlefile_selected() {
+  yield do_test({value: true, result: "testfile_bug1251809"});
+});
+
+add_task(function* test_title_set() {
+  yield do_test({title: "foo", result: "foo"});
+});
+
+add_task(function* test_nofile_selected() {
+  yield do_test({result: "No file selected."});
+});
+
+add_task(function* test_multipleset_nofile_selected() {
+  yield do_test({multiple: true, result: "No files selected."});
+});
 
-  for (let test of data) {
-    let input = doc.createElement('input');
+add_task(function* test_requiredset() {
+  yield do_test({required: true, result: "Please select a file."});
+});
+
+function* do_test(test) {
+  info(`starting test ${JSON.stringify(test)}`);
+
+  let MockFilePicker = SpecialPowers.MockFilePicker;
+  MockFilePicker.init(window);
+  MockFilePicker.returnValue = MockFilePicker.returnOK;
+  if (test.value) {
+    MockFilePicker.displayDirectory = FileUtils.getDir("TmpD", [], false);
+    MockFilePicker.returnFiles = [tempFile];
+  }
+
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+  yield new Promise(resolve => {
+    EventUtils.synthesizeNativeMouseMove(tab.linkedBrowser, 300, 300, resolve);
+  });
+
+  ContentTask.spawn(tab.linkedBrowser, test, function*(test) {
+    let doc = content.document;
+    let input = doc.createElement("input");
     doc.body.appendChild(input);
-    input.type = 'file';
+    input.id = "test_input";
+    input.setAttribute("style", "position: absolute; top: 0; left: 0;");
+    input.clientTop; // Flush layout
+    input.type = "file";
     if (test.title) {
-      input.setAttribute('title', test.title);
-    }
-    if (test.value) {
-      if (test.value == "/tmp" && navigator.platform.indexOf('Win') != -1) {
-        test.value = "C:\\Temp";
-        test.result = "Temp";
-      }
-      input.value = test.value;
+      input.setAttribute("title", test.title);
     }
     if (test.multiple) {
       input.multiple = true;
     }
     if (test.required) {
       input.required = true;
     }
+  });
 
-    ok(tooltip.fillInPageTooltip(input));
-    is(tooltip.getAttribute('label'), test.result);
+  if (test.value) {
+    // Open the File Picker dialog (MockFilePicker) to select
+    // the files for the test.
+    yield BrowserTestUtils.synthesizeMouseAtCenter("#test_input", {}, tab.linkedBrowser);
+    yield ContentTask.spawn(tab.linkedBrowser, {}, function*() {
+      let input = content.document.querySelector("#test_input");
+      yield ContentTaskUtils.waitForCondition(() => input.files.length,
+        "The input should have at least one file selected");
+      info(`The input has ${input.files.length} file(s) selected.`);
+    });
   }
+
+  let awaitTooltipOpen = new Promise(resolve => {
+    let tooltipId = Services.appinfo.browserTabsRemoteAutostart ?
+                      "remoteBrowserTooltip" :
+                      "aHTMLTooltip";
+    let tooltip = document.getElementById(tooltipId);
+    tooltip.addEventListener("popupshown", function onpopupshown(event) {
+      tooltip.removeEventListener("popupshown", onpopupshown);
+      resolve(event.target);
+    });
+  });
+  yield new Promise(resolve => {
+    EventUtils.synthesizeNativeMouseMove(tab.linkedBrowser, 100, 5, resolve);
+  });
+  yield new Promise(resolve => setTimeout(resolve, 100));
+  yield new Promise(resolve => {
+    EventUtils.synthesizeNativeMouseMove(tab.linkedBrowser, 110, 15, resolve);
+  });
+  let tooltip = yield awaitTooltipOpen;
+
+  is(tooltip.getAttribute("label"), test.result, "tooltip label should match expectation");
+
+  yield BrowserTestUtils.removeTab(tab);
 }
+
+function createTempFile() {
+  let file = FileUtils.getDir("TmpD", [], false);
+  file.append("testfile_bug1251809");
+  file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+  return file;
+}
--- a/toolkit/content/widgets/popup.xml
+++ b/toolkit/content/widgets/popup.xml
@@ -532,18 +532,19 @@
            Note that DefaultTooltipTextProvider::GetNodeText() from nsDocShellTreeOwner.cpp
            also performs the same function, but for embedded clients that don't use a XUL/JS
            layer. These two should be kept synchronized.
         -->
       <method name="fillInPageTooltip">
         <parameter name="tipElement"/>
         <body>
         <![CDATA[
-          // Don't show the tooltip if the tooltip node is a document or disconnected.
+          // Don't show the tooltip if the tooltip node is a document, browser, or disconnected.
           if (!tipElement || !tipElement.ownerDocument ||
+              tipElement.tagName == "xul:browser" ||
               (tipElement.ownerDocument.compareDocumentPosition(tipElement) & document.DOCUMENT_POSITION_DISCONNECTED)) {
             return false;
           }
 
           var defView = tipElement.ownerDocument.defaultView;
           // XXX Work around bug 350679:
           // "Tooltips can be fired in documents with no view".
           if (!defView)