Bug 1386316 - Resize Android WebExtension Options UI iframe to match its content size. r=mixedpuppy
authorLuca Greco <lgreco@mozilla.com>
Mon, 07 Aug 2017 15:45:55 +0200
changeset 374870 e5ae7beff73240132c3b9ba3bbeda44f8eb6617e
parent 374869 05a5b7e96f58d7a25b47f4c96c2935233b2c45ed
child 374871 0ecb6589f5dbfa128e17e495c9184d329255e8d8
push id48844
push userluca.greco@alcacoop.it
push dateTue, 15 Aug 2017 22:39:03 +0000
treeherderautoland@e5ae7beff732 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmixedpuppy
bugs1386316
milestone57.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1386316 - Resize Android WebExtension Options UI iframe to match its content size. r=mixedpuppy MozReview-Commit-ID: 17a240drasZ
mobile/android/chrome/content/aboutAddons.js
mobile/android/chrome/content/aboutAddons.xhtml
mobile/android/components/extensions/test/mochitest/chrome.ini
mobile/android/components/extensions/test/mochitest/test_ext_options_ui.html
mobile/android/themes/core/aboutAddons.css
--- a/mobile/android/chrome/content/aboutAddons.js
+++ b/mobile/android/chrome/content/aboutAddons.js
@@ -140,16 +140,20 @@ function onPopState(aEvent) {
 
 function showAddons() {
   // Hide the addon options and show the addons list
   let details = document.querySelector("#addons-details");
   details.classList.add("hidden");
   let list = document.querySelector("#addons-list");
   list.classList.remove("hidden");
   document.documentElement.removeAttribute("details");
+
+  // Clean the optionsBox content when switching to the add-ons list view.
+  let optionsBox = document.querySelector("#addons-details > .addon-item .options-box");
+  optionsBox.innerHTML = "";
 }
 
 function showAddonOptions() {
   // Hide the addon list and show the addon options
   let list = document.querySelector("#addons-list");
   list.classList.add("hidden");
   let details = document.querySelector("#addons-details");
   details.classList.remove("hidden");
@@ -354,35 +358,70 @@ var Addons = {
     let optionsURL = aListItem.getAttribute("optionsURL");
 
     // Always clean the options content before rendering the options of the
     // newly selected extension.
     optionsBox.innerHTML = "";
 
     switch (parseInt(addon.optionsType)) {
       case AddonManager.OPTIONS_TYPE_INLINE_BROWSER:
+        // Allow the options to use all the available width space.
+        optionsBox.classList.remove("inner");
+
         // WebExtensions are loaded asynchronously and the optionsURL
         // may not be available via listitem when the add-on has just been
         // installed, but it is available on the addon if one is set.
         detailItem.setAttribute("optionsURL", addon.optionsURL);
         this.createWebExtensionOptions(optionsBox, addon.optionsURL, addon.optionsBrowserStyle);
         break;
       case AddonManager.OPTIONS_TYPE_INLINE:
+        // Keep the usual layout for any options related the legacy (or system) add-ons.
+        optionsBox.classList.add("inner");
+
         this.createInlineOptions(optionsBox, optionsURL, aListItem);
         break;
     }
 
     showAddonOptions();
   },
 
   createWebExtensionOptions: async function(destination, optionsURL, browserStyle) {
+    let originalHeight;
     let frame = document.createElement("iframe");
     frame.setAttribute("id", "addon-options");
     frame.setAttribute("mozbrowser", "true");
+    frame.setAttribute("style", "width: 100%; overflow: hidden;");
+
+    // Adjust iframe height to the iframe content (also between navigation of multiple options
+    // files).
+    frame.onload = (evt) => {
+      if (evt.target !== frame) {
+        return;
+      }
+
+      const {document} = frame.contentWindow;
+      const bodyScrollHeight = document.body && document.body.scrollHeight;
+      const documentScrollHeight = document.documentElement.scrollHeight;
+
+      // Set the iframe height to the maximum between the body and the document
+      // scrollHeight values.
+      frame.style.height = Math.max(bodyScrollHeight, documentScrollHeight) + "px";
+
+      // Restore the original iframe height between option page loads,
+      // so that we don't force the new document to have the same size
+      // of the previosuly loaded option page.
+      frame.contentWindow.addEventListener("unload", () => {
+        frame.style.height = originalHeight + "px";
+      }, {once: true});
+    };
+
     destination.appendChild(frame);
+
+    originalHeight = frame.getBoundingClientRect().height;
+
     // Loading the URL this way prevents the native back
     // button from applying to the iframe.
     frame.contentWindow.location.replace(optionsURL);
   },
 
   createInlineOptions(destination, optionsURL, aListItem) {
     // This function removes and returns the text content of aNode without
     // removing any child elements. Removing the text nodes ensures any XBL
--- a/mobile/android/chrome/content/aboutAddons.xhtml
+++ b/mobile/android/chrome/content/aboutAddons.xhtml
@@ -39,19 +39,19 @@
   <div id="addons-details" class="list hidden">
     <div class="addon-item list-item">
       <img class="icon"/>
       <div class="inner">
         <div class="details">
           <div class="title"></div><div class="version"></div>
         </div>
         <div class="description-full"></div>
-        <div class="options-box"></div>
       </div>
       <div class="warn-unsigned">&addonUnsigned.message; <a id="unsigned-learn-more">&addonUnsigned.learnMore;</a></div>
+      <div class="options-box"></div>
       <div class="status status-uninstalled show-on-uninstall"></div>
       <div class="buttons">
         <button id="enable-btn" class="show-on-disable hide-on-enable hide-on-uninstall" >&addonAction.enable;</button>
         <button id="disable-btn" class="show-on-enable hide-on-disable hide-on-uninstall" >&addonAction.disable;</button>
         <button id="uninstall-btn" class="hide-on-uninstall" >&addonAction.uninstall;</button>
         <button id="cancel-btn" class="show-on-uninstall" >&addonAction.undo;</button>
       </div>
     </div>
--- a/mobile/android/components/extensions/test/mochitest/chrome.ini
+++ b/mobile/android/components/extensions/test/mochitest/chrome.ini
@@ -3,11 +3,12 @@ support-files =
   head.js
   ../../../../../../toolkit/components/extensions/test/mochitest/chrome_cleanup_script.js
 tags = webextensions
 
 [test_ext_browserAction_getTitle_setTitle.html]
 [test_ext_browserAction_onClicked.html]
 [test_ext_browsingData_cookies_cache.html]
 [test_ext_browsingData_settings.html]
+[test_ext_options_ui.html]
 [test_ext_pageAction_show_hide.html]
 [test_ext_pageAction_getPopup_setPopup.html]
 skip-if = os == 'android' # bug 1373170
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_options_ui.html
@@ -0,0 +1,196 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>PageAction Test</title>
+  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+  <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="head.js"></script>
+  <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://testing-common/ContentTask.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+async function waitAboutAddonsRendered(addonId) {
+  await ContentTaskUtils.waitForCondition(() => {
+    return content.document.querySelector(`div.addon-item[addonID="${addonId}"]`);
+  }, `wait Addon Item for ${addonId} to be rendered`);
+}
+
+async function navigateToAddonDetails(addonId) {
+  const item = content.document.querySelector(`div.addon-item[addonID="${addonId}"]`);
+  let rect = item.getBoundingClientRect();
+  const x = rect.left + rect.width / 2;
+  const y = rect.top + rect.height / 2;
+  let domWinUtils = content.window.QueryInterface(Ci.nsIInterfaceRequestor)
+                           .getInterface(Ci.nsIDOMWindowUtils);
+
+  domWinUtils.sendMouseEventToWindow("mousedown", x, y, 0, 1, 0);
+  domWinUtils.sendMouseEventToWindow("mouseup", x, y, 0, 1, 0);
+}
+
+async function waitAddonOptionsPage([addonId, expectedText]) {
+  await ContentTaskUtils.waitForCondition(() => {
+    const optionsIframe = content.document.querySelector(`#addon-options`);
+    return optionsIframe && optionsIframe.contentDocument.readyState === "complete" &&
+           optionsIframe.contentDocument.body.innerText.includes(expectedText);
+  }, `wait Addon Options ${expectedText} for ${addonId} to be loaded`);
+
+  const optionsIframe = content.document.querySelector(`#addon-options`);
+
+  return {
+    iframeHeight: optionsIframe.style.height,
+    documentHeight: optionsIframe.contentDocument.documentElement.scrollHeight,
+    bodyHeight: optionsIframe.contentDocument.body.scrollHeight,
+  };
+}
+
+async function clickOnLinkInOptionsPage(selector) {
+  const optionsIframe = content.document.querySelector(`#addon-options`);
+  optionsIframe.contentDocument.querySelector(selector).click();
+}
+
+async function navigateBack() {
+  content.window.history.back();
+}
+
+add_task(async function test_options_ui_iframe_height() {
+  let addonID = "test-options-ui@mozilla.org";
+
+  let extension = ExtensionTestUtils.loadExtension({
+    useAddonManager: "temporary",
+    manifest: {
+      applications: {
+        gecko: {id: addonID},
+      },
+      name: "Options UI Extension",
+      description: "Longer addon description",
+      options_ui: {
+        page: "options.html",
+      },
+    },
+    files: {
+      // An option page with the document element bigger than the body.
+      "options.html": `<!DOCTYPE html>
+        <html>
+          <head>
+            <meta charset="utf-8">
+            <style>
+              html { height: 500px; border: 1px solid black; }
+              body { height: 200px; }
+            </style>
+          </head>
+          <body>
+            <h1>Options page 1</h1>
+            <a href="options2.html">go to page 2</a>
+          </body>
+        </html>
+      `,
+      // A second option page with the body element bigger than the document.
+      "options2.html": `<!DOCTYPE html>
+        <html>
+          <head>
+            <meta charset="utf-8">
+            <style>
+              html { height: 200px; border: 1px solid black; }
+              body { height: 350px; }
+            </style>
+          </head>
+          <body>
+            <h1>Options page 2</h1>
+          </body>
+        </html>
+      `,
+    },
+  });
+
+  await extension.startup();
+
+  let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+  let BrowserApp = chromeWin.BrowserApp;
+
+  let waitAboutAddonsLoaded = new Promise(resolve => {
+    let listener = (event) => {
+      if (event.target.defaultView.location.href === "about:addons") {
+        BrowserApp.deck.removeEventListener("DOMContentLoaded", listener);
+        resolve();
+      }
+    };
+
+    BrowserApp.deck.addEventListener("DOMContentLoaded", listener);
+  });
+
+  BrowserApp.selectOrAddTab("about:addons", {
+    selected: true,
+    parentId: BrowserApp.selectedTab.id,
+  });
+
+  await waitAboutAddonsLoaded;
+
+  is(BrowserApp.selectedTab.currentURI.spec, "about:addons",
+     "about:addons is the currently selected tab");
+
+  let aboutAddonsWindow = BrowserApp.selectedTab.browser.contentWindow;
+
+  is(aboutAddonsWindow.location.href, "about:addons");
+
+  await ContentTask.spawn(BrowserApp.selectedTab.browser, addonID, waitAboutAddonsRendered);
+
+  await ContentTask.spawn(BrowserApp.selectedTab.browser, addonID, navigateToAddonDetails);
+
+  const optionsSizes = await ContentTask.spawn(
+    BrowserApp.selectedTab.browser, [addonID, "Options page 1"], waitAddonOptionsPage
+  );
+
+  ok(parseInt(optionsSizes.iframeHeight, 10) >= 500,
+     "The addon options iframe is at least 500px");
+
+  is(optionsSizes.iframeHeight, optionsSizes.documentHeight + "px",
+     "The addon options iframe has the expected height");
+
+  await ContentTask.spawn(BrowserApp.selectedTab.browser, "a", clickOnLinkInOptionsPage);
+
+  const options2Sizes = await ContentTask.spawn(
+    BrowserApp.selectedTab.browser, [addonID, "Options page 2"], waitAddonOptionsPage
+  );
+
+  // The second option page has a body bigger than the document element
+  // and we expect the iframe to be bigger than that.
+  ok(parseInt(options2Sizes.iframeHeight, 10) > 200,
+     `The iframe is bigger then 200px (${options2Sizes.iframeHeight})`);
+
+  // The second option page has a body smaller than the document element of the first
+  // page and we expect the iframe to be smaller than for the previous options page.
+  ok(parseInt(options2Sizes.iframeHeight, 10) < 500,
+     `The iframe is smaller then 500px (${options2Sizes.iframeHeight})`);
+
+  is(options2Sizes.iframeHeight, options2Sizes.documentHeight + "px",
+     "The second addon options page has the expected height");
+
+  await ContentTask.spawn(BrowserApp.selectedTab.browser, null, navigateBack);
+
+  const backToOptionsSizes =  await ContentTask.spawn(
+    BrowserApp.selectedTab.browser, [addonID, "Options page 1"], waitAddonOptionsPage
+  );
+
+  // After going back to the first options page,
+  // we expect the iframe to have the same size of the previous load.
+  is(backToOptionsSizes.iframeHeight, optionsSizes.iframeHeight,
+     `When navigating back, the old iframe size is restored (${backToOptionsSizes.iframeHeight})`);
+
+  BrowserApp.closeTab(BrowserApp.selectedTab);
+
+  await extension.unload();
+});
+
+</script>
+
+</body>
+</html>
--- a/mobile/android/themes/core/aboutAddons.css
+++ b/mobile/android/themes/core/aboutAddons.css
@@ -44,16 +44,19 @@ a:active {
   width: 100%;
   overflow: hidden;
   white-space: nowrap;
   text-overflow: ellipsis;
 }
 
 .warn-unsigned {
   border-top: 1px solid var(--color_about_item_border);
+  border-bottom: 1px solid var(--color_about_item_border);
+  margin-top: 1em;
+  margin-bottom: 1em;
   padding: 1em;
   padding-inline-start: calc(var(--icon-size) + var(--icon-margin) * 2);
   background-image: url("chrome://browser/skin/images/grey-caution.svg");
   background-size: var(--icon-size);
   background-position: var(--icon-margin);
   background-repeat: no-repeat;
   display: none;
 }