Bug 1251809 - Add input[type=file] tooltip support for e10s. r=ehsan
authorJared Wein <jwein@mozilla.com>
Wed, 16 Mar 2016 19:07:51 -0400
changeset 288956 d31323858727
parent 288955 d2ce28624461
child 288957 a4c6d6add2fc
push id18218
push userjwein@mozilla.com
push dateWed, 16 Mar 2016 23:08:24 +0000
treeherderfx-team@d31323858727 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersehsan
bugs1251809
milestone48.0a1
Bug 1251809 - Add input[type=file] tooltip support for e10s. r=ehsan MozReview-Commit-ID: FpwKGrFQNrK
dom/base/nsIDOMFileList.idl
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/dom/base/nsIDOMFileList.idl
+++ b/dom/base/nsIDOMFileList.idl
@@ -1,14 +1,14 @@
 /* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsISupports.idl"
 
-[uuid(57128a85-34de-42db-a252-84dd57724a59)]
+[builtinclass, uuid(57128a85-34de-42db-a252-84dd57724a59)]
 interface nsIDOMFileList : nsISupports
 {
   readonly attribute unsigned long length;
   // returns a DOM File object
   nsISupports item(in unsigned long index);
 };
--- 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)
@@ -1066,30 +1070,26 @@ public:
   DefaultTooltipTextProvider();
 
   NS_DECL_ISUPPORTS
   NS_DECL_NSITOOLTIPTEXTPROVIDER
 
 protected:
   ~DefaultTooltipTextProvider() {}
 
-  nsCOMPtr<nsIAtom> mTag_dialog;
-  nsCOMPtr<nsIAtom> mTag_dialogheader;
-  nsCOMPtr<nsIAtom> mTag_window;
+  nsCOMPtr<nsIAtom> mTag_dialogHeader;
 };
 
 NS_IMPL_ISUPPORTS(DefaultTooltipTextProvider, nsITooltipTextProvider)
 
 DefaultTooltipTextProvider::DefaultTooltipTextProvider()
 {
   // There are certain element types which we don't want to use
   // as tool tip text.
-  mTag_dialog = do_GetAtom("dialog");
-  mTag_dialogheader = do_GetAtom("dialogheader");
-  mTag_window = do_GetAtom("window");
+  mTag_dialogHeader = do_GetAtom("dialogheader");
 }
 
 // A helper routine that determines whether we're still interested in SVG
 // titles. We need to stop at the SVG root element that has a document node
 // parent.
 static bool
 UseSVGTitle(nsIDOMElement* aCurrElement)
 {
@@ -1137,23 +1137,76 @@ DefaultTooltipTextProvider::GetNodeText(
     }
   }
 
   while (!found && current) {
     nsCOMPtr<nsIDOMElement> currElement(do_QueryInterface(current));
     if (currElement) {
       nsCOMPtr<nsIContent> content(do_QueryInterface(currElement));
       if (content) {
-        if (!content->IsAnyOfXULElements(mTag_dialog,
-                                         mTag_dialogheader,
-                                         mTag_window)) {
+        if (!content->IsAnyOfXULElements(nsGkAtoms::dialog,
+                                         mTag_dialogHeader,
+                                         nsGkAtoms::window)) {
           // first try the normal title attribute...
           if (!content->IsSVGElement()) {
-            currElement->GetAttribute(NS_LITERAL_STRING("title"), outText);
-            if (outText.Length()) {
+            // If the element is an <input type="file"> without a title,
+            // we should show the current file selection.
+            if (content->IsHTMLElement(nsGkAtoms::input) &&
+                content->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+                                     NS_LITERAL_STRING("file"), eIgnoreCase) &&
+                !content->HasAttr(kNameSpaceID_None, nsGkAtoms::title)) {
+              found = true;
+              nsCOMPtr<nsIDOMFileList> fileList;
+              nsCOMPtr<nsIDOMHTMLInputElement> currInputElement(do_QueryInterface(currElement));
+              nsresult rv = currInputElement->GetFiles(getter_AddRefs(fileList));
+              NS_ENSURE_SUCCESS(rv, rv);
+              if (!fileList) {
+                return NS_ERROR_FAILURE;
+              }
+
+              nsCOMPtr<nsIStringBundleService> bundleService =
+                mozilla::services::GetStringBundleService();
+              if (!bundleService) {
+                return NS_ERROR_FAILURE;
+              }
+
+              nsCOMPtr<nsIStringBundle> bundle;
+              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) {
+                if (content->HasAttr(kNameSpaceID_None, nsGkAtoms::multiple)) {
+                  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
@@ -13,17 +13,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,111 @@
-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."});
+});
 
-  for (let test of data) {
-    let input = doc.createElement('input');
+add_task(function* test_multipleset_nofile_selected() {
+  yield do_test({multiple: true, result: "No files selected."});
+});
+
+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 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.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) {
+    let MockFilePicker = SpecialPowers.MockFilePicker;
+    MockFilePicker.init(window);
+    MockFilePicker.returnValue = MockFilePicker.returnOK;
+    MockFilePicker.displayDirectory = FileUtils.getDir("TmpD", [], false);
+    MockFilePicker.returnFiles = [tempFile];
+
+    try {
+      // 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.`);
+      });
+    } finally {
+      MockFilePicker.cleanup();
+    }
   }
+
+  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
@@ -533,18 +533,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.localName == "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)