Merge mozilla-central to inbound. a=merge CLOSED TREE
authorMargareta Eliza Balazs <ebalazs@mozilla.com>
Tue, 16 Oct 2018 12:39:05 +0300
changeset 489756 db589d2f37284b67611b56d969b4bc9442697002
parent 489722 d37fe76d61f968dc7e65327690020c6069841adb (current diff)
parent 489755 9079bbe837184ed183b133a374753865b6768bc4 (diff)
child 489757 2c7473068da142246c65c671387eaa8108c04ae3
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
reviewersmerge
milestone64.0a1
Merge mozilla-central to inbound. a=merge CLOSED TREE
--- a/browser/components/sessionstore/test/browser_248970_b_perwindowpb.js
+++ b/browser/components/sessionstore/test/browser_248970_b_perwindowpb.js
@@ -5,17 +5,17 @@
 function test() {
   /** Test (B) for Bug 248970 **/
   waitForExplicitFinish();
 
   let windowsToClose = [];
   let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
   let filePath = file.path;
   let fieldList = {
-    "//input[@name='input']":     Date.now().toString(),
+    "//input[@name='input']":     Date.now().toString(16),
     "//input[@name='spaced 1']":  Math.random().toString(),
     "//input[3]":                 "three",
     "//input[@type='checkbox']":  true,
     "//input[@name='uncheck']":   false,
     "//input[@type='radio'][1]":  false,
     "//input[@type='radio'][2]":  true,
     "//input[@type='radio'][3]":  false,
     "//select":                   2,
--- a/browser/components/sessionstore/test/browser_463206.js
+++ b/browser/components/sessionstore/test/browser_463206.js
@@ -17,17 +17,17 @@ add_task(async function() {
     function typeText(aTextField, aValue) {
       aTextField.value = aValue;
 
       let event = aTextField.ownerDocument.createEvent("UIEvents");
       event.initUIEvent("input", true, true, aTextField.ownerGlobal, 0);
       aTextField.dispatchEvent(event);
     }
 
-    typeText(content.document.getElementById("out1"), Date.now());
+    typeText(content.document.getElementById("out1"), Date.now().toString(16));
     typeText(content.document.getElementsByName("1|#out2")[0], Math.random());
     typeText(content.frames[0].frames[1].document.getElementById("in1"), new Date());
   });
 
   // Duplicate the tab.
   let tab2 = gBrowser.duplicateTab(tab);
   await promiseTabRestored(tab2);
 
--- a/browser/components/sessionstore/test/browser_formdata_xpath.js
+++ b/browser/components/sessionstore/test/browser_formdata_xpath.js
@@ -16,17 +16,17 @@ add_task(function setup() {
     Services.prefs.clearUserPref("browser.sessionstore.privacy_level");
   });
 });
 
 const FILE1 = createFilePath("346337_test1.file");
 const FILE2 = createFilePath("346337_test2.file");
 
 const FIELDS = {
-  "//input[@name='input']":     Date.now().toString(),
+  "//input[@name='input']":     Date.now().toString(16),
   "//input[@name='spaced 1']":  Math.random().toString(),
   "//input[3]":                 "three",
   "//input[@type='checkbox']":  true,
   "//input[@name='uncheck']":   false,
   "//input[@type='radio'][1]":  false,
   "//input[@type='radio'][2]":  true,
   "//input[@type='radio'][3]":  false,
   "//select":                   2,
--- a/browser/components/urlbar/UrlbarInput.jsm
+++ b/browser/components/urlbar/UrlbarInput.jsm
@@ -39,16 +39,17 @@ class UrlbarInput {
    *   Intended for use in unit tests only.
    */
   constructor(options = {}) {
     this.textbox = options.textbox;
     this.textbox.clickSelectsAll = UrlbarPrefs.get("clickSelectsAll");
 
     this.panel = options.panel;
     this.window = this.textbox.ownerGlobal;
+    this.document = this.window.document;
     this.controller = options.controller || new UrlbarController({
       window: this.window,
     });
     this.view = new UrlbarView(this);
     this.valueIsTyped = false;
     this.userInitiatedFocus = false;
     this.isPrivate = PrivateBrowsingUtils.isWindowPrivate(this.window);
 
@@ -233,16 +234,21 @@ class UrlbarInput {
   }
 
   // Getters and Setters below.
 
   get focused() {
     return this.textbox.getAttribute("focused") == "true";
   }
 
+  get goButton() {
+    return this.document.getAnonymousElementByAttribute(this.textbox, "anonid",
+      "urlbar-go-button");
+  }
+
   get value() {
     return this.inputField.value;
   }
 
   set value(val) {
     val = this.trimValue(val);
 
     this.valueIsTyped = false;
@@ -514,17 +520,17 @@ class CopyCutController {
 
     if (command == "cmd_cut" && this.isCommandEnabled(command)) {
       let start = urlbar.selectionStart;
       let end = urlbar.selectionEnd;
       urlbar.inputField.value = urlbar.inputField.value.substring(0, start) +
                                 urlbar.inputField.value.substring(end);
       urlbar.selectionStart = urlbar.selectionEnd = start;
 
-      let event = urlbar.window.document.createEvent("UIEvents");
+      let event = urlbar.document.createEvent("UIEvents");
       event.initUIEvent("input", true, false, this.window, 0);
       urlbar.dispatchEvent(event);
 
       urlbar.window.SetPageProxyState("invalid");
     }
 
     ClipboardHelper.copyString(val);
   }
--- a/devtools/client/preferences/devtools-client.js
+++ b/devtools/client/preferences/devtools-client.js
@@ -53,18 +53,18 @@ pref("devtools.inspector.shapesHighlight
 // Enable the Font Editor
 pref("devtools.inspector.fonteditor.enabled", true);
 // Enable the font highlight-on-hover feature
 pref("devtools.inspector.fonthighlighter.enabled", true);
 // Enable tracking of style changes and the Changes panel in the Inspector
 pref("devtools.inspector.changes.enabled", false);
 
 // Flexbox preferences
-// Enable the Flexbox highlighter and inspector panel in Nightly
-#if defined(NIGHTLY_BUILD)
+// Enable the Flexbox highlighter and inspector panel in Nightly and DevEdition
+#if defined(NIGHTLY_BUILD) || defined(MOZ_DEV_EDITION)
 pref("devtools.inspector.flexboxHighlighter.enabled", true);
 pref("devtools.flexboxinspector.enabled", true);
 #else
 pref("devtools.inspector.flexboxHighlighter.enabled", false);
 pref("devtools.flexboxinspector.enabled", false);
 #endif
 
 // Grid highlighter preferences
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -7297,20 +7297,20 @@ nsIDocument::GetViewportInfo(const Scree
         return nsViewportInfo(aDisplaySize, defaultScale,
                               /*allowZoom*/true);
       }
     }
 
     nsAutoString minScaleStr;
     GetHeaderData(nsGkAtoms::viewport_minimum_scale, minScaleStr);
 
-    nsresult errorCode;
-    mScaleMinFloat = LayoutDeviceToScreenScale(minScaleStr.ToFloat(&errorCode));
-
-    if (NS_FAILED(errorCode)) {
+    nsresult scaleMinErrorCode;
+    mScaleMinFloat = LayoutDeviceToScreenScale(minScaleStr.ToFloat(&scaleMinErrorCode));
+
+    if (NS_FAILED(scaleMinErrorCode)) {
       mScaleMinFloat = kViewportMinScale;
     }
 
     mScaleMinFloat = mozilla::clamped(
         mScaleMinFloat, kViewportMinScale, kViewportMaxScale);
 
     nsAutoString maxScaleStr;
     GetHeaderData(nsGkAtoms::viewport_maximum_scale, maxScaleStr);
@@ -7319,16 +7319,22 @@ nsIDocument::GetViewportInfo(const Scree
     // because they are used later (see the width calculations).
     nsresult scaleMaxErrorCode;
     mScaleMaxFloat = LayoutDeviceToScreenScale(maxScaleStr.ToFloat(&scaleMaxErrorCode));
 
     if (NS_FAILED(scaleMaxErrorCode)) {
       mScaleMaxFloat = kViewportMaxScale;
     }
 
+    // Resolve min-zoom and max-zoom values.
+    // https://drafts.csswg.org/css-device-adapt/#constraining-min-max-zoom
+    if (NS_SUCCEEDED(scaleMaxErrorCode) && NS_SUCCEEDED(scaleMinErrorCode)) {
+      mScaleMaxFloat = std::max(mScaleMinFloat, mScaleMaxFloat);
+    }
+
     mScaleMaxFloat = mozilla::clamped(
         mScaleMaxFloat, kViewportMinScale, kViewportMaxScale);
 
     nsAutoString scaleStr;
     GetHeaderData(nsGkAtoms::viewport_initial_scale, scaleStr);
 
     nsresult scaleErrorCode;
     mScaleFloat = LayoutDeviceToScreenScale(scaleStr.ToFloat(&scaleErrorCode));
--- a/dom/base/nsViewportInfo.h
+++ b/dom/base/nsViewportInfo.h
@@ -22,39 +22,39 @@ static const mozilla::CSSIntSize kViewpo
  * nsIDocument::GetViewportInfo for more information on this functionality.
  */
 class MOZ_STACK_CLASS nsViewportInfo
 {
   public:
     nsViewportInfo(const mozilla::ScreenIntSize& aDisplaySize,
                    const mozilla::CSSToScreenScale& aDefaultZoom,
                    bool aAllowZoom) :
+      mDefaultZoom(aDefaultZoom),
       mDefaultZoomValid(true),
-      mDefaultZoom(aDefaultZoom),
       mAutoSize(true),
       mAllowZoom(aAllowZoom)
     {
         mSize = mozilla::ScreenSize(aDisplaySize) / mDefaultZoom;
         mozilla::CSSToLayoutDeviceScale pixelRatio(1.0f);
         mMinZoom = pixelRatio * kViewportMinScale;
         mMaxZoom = pixelRatio * kViewportMaxScale;
         ConstrainViewportValues();
     }
 
     nsViewportInfo(const mozilla::CSSToScreenScale& aDefaultZoom,
                    const mozilla::CSSToScreenScale& aMinZoom,
                    const mozilla::CSSToScreenScale& aMaxZoom,
                    const mozilla::CSSSize& aSize,
                    bool aAutoSize,
                    bool aAllowZoom) :
-                     mDefaultZoomValid(true),
                      mDefaultZoom(aDefaultZoom),
                      mMinZoom(aMinZoom),
                      mMaxZoom(aMaxZoom),
                      mSize(aSize),
+                     mDefaultZoomValid(true),
                      mAutoSize(aAutoSize),
                      mAllowZoom(aAllowZoom)
     {
       ConstrainViewportValues();
     }
 
     bool IsDefaultZoomValid() const { return mDefaultZoomValid; }
     mozilla::CSSToScreenScale GetDefaultZoom() const { return mDefaultZoom; }
@@ -70,33 +70,33 @@ class MOZ_STACK_CLASS nsViewportInfo
 
     /**
      * Constrain the viewport calculations from the
      * nsIDocument::GetViewportInfo() function in order to always return
      * sane minimum/maximum values.
      */
     void ConstrainViewportValues();
 
-    // If the default zoom was specified and was between the min and max
-    // zoom values.
-    bool mDefaultZoomValid;
-
     // Default zoom indicates the level at which the display is 'zoomed in'
     // initially for the user, upon loading of the page.
     mozilla::CSSToScreenScale mDefaultZoom;
 
     // The minimum zoom level permitted by the page.
     mozilla::CSSToScreenScale mMinZoom;
 
     // The maximum zoom level permitted by the page.
     mozilla::CSSToScreenScale mMaxZoom;
 
     // The size of the viewport, specified by the <meta name="viewport"> tag.
     mozilla::CSSSize mSize;
 
+    // If the default zoom was specified and was between the min and max
+    // zoom values.
+    bool mDefaultZoomValid;
+
     // Whether or not we should automatically size the viewport to the device's
     // width. This is true if the document has been optimized for mobile, and
     // the width property of a specified <meta name="viewport"> tag is either
     // not specified, or is set to the special value 'device-width'.
     bool mAutoSize;
 
     // Whether or not the user can zoom in and out on the page. Default is true.
     bool mAllowZoom;
--- a/dom/base/test/mochitest.ini
+++ b/dom/base/test/mochitest.ini
@@ -680,31 +680,24 @@ skip-if = (verify && (os == 'win' || os 
 [test_integer_attr_with_leading_zero.html]
 [test_intersectionobservers.html]
 [test_link_prefetch.html]
 skip-if = !e10s # Track Bug 1281415
 [test_link_preload.html]
 [test_link_stylesheet.html]
 [test_messagemanager_targetchain.html]
 [test_meta_viewport0.html]
-skip-if = (os != 'android')    # meta-viewport tag support is mobile-only
 [test_meta_viewport1.html]
-skip-if = (os != 'android')    # meta-viewport tag support is mobile-only
 [test_meta_viewport2.html]
-skip-if = (os != 'android')    # meta-viewport tag support is mobile-only
 [test_meta_viewport3.html]
-skip-if = (os != 'android')    # meta-viewport tag support is mobile-only
 [test_meta_viewport4.html]
-skip-if = (os != 'android')    # meta-viewport tag support is mobile-only
 [test_meta_viewport5.html]
-skip-if = (os != 'android')    # meta-viewport tag support is mobile-only
 [test_meta_viewport6.html]
-skip-if = (os != 'android')    # meta-viewport tag support is mobile-only
 [test_meta_viewport7.html]
-skip-if = (os != 'android')    # meta-viewport tag support is mobile-only
+[test_meta_viewport_maximum_scale_0.html]
 [test_mozbrowser_apis_blocked.html]
 [test_mozMatchesSelector.html]
 [test_mutationobserver_anonymous.html]
 [test_mutationobservers.html]
 [test_named_frames.html]
 [test_navigator_hardwareConcurrency.html]
 [test_navigator_language.html]
 [test_navigatorPrefOverride.html]
--- a/dom/base/test/test_meta_viewport0.html
+++ b/dom/base/test/test_meta_viewport0.html
@@ -1,81 +1,46 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>meta viewport test</title>
   <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/AddTask.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
   <script src="viewport_helpers.js"></script>
 </head>
 <body>
   <p>No &lt;meta name="viewport"&gt; tag</p>
   <script type="application/javascript">
     "use strict";
 
-    SimpleTest.waitForExplicitFinish();
-
-    let tests = [];
-
     function fuzzeq(a, b, msg) {
       ok(Math.abs(a - b) < 1e-6, msg);
     }
 
-    tests.push(function test1() {
-      SpecialPowers.pushPrefEnv(scaleRatio(1.0),
-        function() {
-          let info = getViewportInfo(800, 480);
-          fuzzeq(info.defaultZoom, 0.1, "initial scale is unspecified");
-          fuzzeq(info.minZoom, 0.1,     "minimum scale defaults to the absolute minimum");
-          is(info.maxZoom,     10,      "maximum scale defaults to the absolute maximum");
-          is(info.width,       980,     "width is the default width");
-          is(info.height,      588,     "height is proportional to displayHeight");
-          is(info.autoSize,    false,   "autoSize is disabled by default");
-          is(info.allowZoom,   true,    "zooming is enabled by default");
+    add_task(async function test1() {
+      await SpecialPowers.pushPrefEnv(scaleRatio(1.0));
 
-          info = getViewportInfo(490, 600);
-          is(info.width,       980,     "width is still the default width");
-          is(info.height,      1200,    "height is proportional to the new displayHeight");
+      let info = getViewportInfo(800, 480);
+      fuzzeq(info.defaultZoom, 0.1, "initial scale is unspecified");
+      fuzzeq(info.minZoom, 0.1,     "minimum scale defaults to the absolute minimum");
+      is(info.maxZoom,     10,      "maximum scale defaults to the absolute maximum");
+      is(info.width,       980,     "width is the default width");
+      is(info.height,      588,     "height is proportional to displayHeight");
+      is(info.autoSize,    false,   "autoSize is disabled by default");
+      is(info.allowZoom,   true,    "zooming is enabled by default");
 
-          nextTest();
-        });
+      info = getViewportInfo(490, 600);
+      is(info.width,       980,     "width is still the default width");
+      is(info.height,      1200,    "height is proportional to the new displayHeight");
     });
 
-    tests.push(function test2() {
-      SpecialPowers.pushPrefEnv(scaleRatio(1.5),
-        function() {
-          let info = getViewportInfo(800, 480);
-          is(info.width,       980,     "width is still the default width");
-          is(info.height,      588,     "height is still proportional to displayHeight");
-
-          nextTest();
-        });
-    });
-
-    function getViewportInfo(aDisplayWidth, aDisplayHeight) {
-      let defaultZoom = {}, allowZoom = {}, minZoom = {}, maxZoom = {},
-          width = {}, height = {}, autoSize = {};
+    add_task(async function test2() {
+      await SpecialPowers.pushPrefEnv(scaleRatio(1.5));
 
-      let cwu = SpecialPowers.getDOMWindowUtils(window);
-      cwu.getViewportInfo(aDisplayWidth, aDisplayHeight, defaultZoom, allowZoom,
-                          minZoom, maxZoom, width, height, autoSize);
-      return {
-        defaultZoom: defaultZoom.value,
-        minZoom: minZoom.value,
-        maxZoom: maxZoom.value,
-        width: width.value,
-        height: height.value,
-        autoSize: autoSize.value,
-        allowZoom: allowZoom.value
-      };
-    }
-
-    function nextTest() {
-      if (tests.length)
-        (tests.shift())();
-      else
-        SimpleTest.finish();
-    }
-    addEventListener("load", nextTest);
+      let info = getViewportInfo(800, 480);
+      is(info.width,       980,     "width is still the default width");
+      is(info.height,      588,     "height is still proportional to displayHeight");
+    });
   </script>
 </body>
 </html>
--- a/dom/base/test/test_meta_viewport1.html
+++ b/dom/base/test/test_meta_viewport1.html
@@ -1,77 +1,42 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>meta viewport test</title>
   <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/AddTask.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <script src="viewport_helpers.js"></script>
 </head>
 <body>
   <p>width=device-width, initial-scale=1</p>
   <script type="application/javascript">
     "use strict";
 
-    SimpleTest.waitForExplicitFinish();
-
-    let tests = [];
+    add_task(async function test1() {
+      await SpecialPowers.pushPrefEnv(scaleRatio(1.0));
 
-    tests.push(function test1() {
-      SpecialPowers.pushPrefEnv(scaleRatio(1.0),
-        function() {
-          let info = getViewportInfo(800, 480);
-          is(info.defaultZoom, 1,    "initial zoom is 100%");
-          is(info.width,       800,  "width is the same as the displayWidth");
-          is(info.height,      480,  "height is the same as the displayHeight");
-          is(info.autoSize,    true, "width=device-width enables autoSize");
-          is(info.allowZoom,   true, "zooming is enabled by default");
+      let info = getViewportInfo(800, 480);
+      is(info.defaultZoom, 1,    "initial zoom is 100%");
+      is(info.width,       800,  "width is the same as the displayWidth");
+      is(info.height,      480,  "height is the same as the displayHeight");
+      is(info.autoSize,    true, "width=device-width enables autoSize");
+      is(info.allowZoom,   true, "zooming is enabled by default");
 
-          info = getViewportInfo(900, 600);
-          is(info.width,       900,  "changing the displayWidth changes the width");
-          is(info.height,      600,  "changing the displayHeight changes the height");
-
-          nextTest();
-        });
+      info = getViewportInfo(900, 600);
+      is(info.width,       900,  "changing the displayWidth changes the width");
+      is(info.height,      600,  "changing the displayHeight changes the height");
     });
 
-    tests.push(function test2() {
-      SpecialPowers.pushPrefEnv(scaleRatio(1.5),
-        function() {
-          let info = getViewportInfo(900, 600);
-          is(info.defaultZoom, 1.5,  "initial zoom is 150%");
-          is(info.width,       600,  "width equals displayWidth/1.5");
-          is(info.height,      400,  "height equals displayHeight/1.5");
-
-          nextTest();
-        });
-    });
-
-    function getViewportInfo(aDisplayWidth, aDisplayHeight) {
-      let defaultZoom = {}, allowZoom = {}, minZoom = {}, maxZoom = {},
-          width = {}, height = {}, autoSize = {};
+    add_task(async function test2() {
+      await SpecialPowers.pushPrefEnv(scaleRatio(1.5));
 
-      let cwu = SpecialPowers.getDOMWindowUtils(window);
-      cwu.getViewportInfo(aDisplayWidth, aDisplayHeight, defaultZoom, allowZoom,
-                          minZoom, maxZoom, width, height, autoSize);
-      return {
-        defaultZoom: defaultZoom.value,
-        minZoom: minZoom.value,
-        maxZoom: maxZoom.value,
-        width: width.value,
-        height: height.value,
-        autoSize: autoSize.value,
-        allowZoom: allowZoom.value
-      };
-    }
-
-    function nextTest() {
-      if (tests.length)
-        (tests.shift())();
-      else
-        SimpleTest.finish();
-    }
-    addEventListener("load", nextTest);
+      let info = getViewportInfo(900, 600);
+      is(info.defaultZoom, 1.5,  "initial zoom is 150%");
+      is(info.width,       600,  "width equals displayWidth/1.5");
+      is(info.height,      400,  "height equals displayHeight/1.5");
+    });
   </script>
 </body>
 </html>
--- a/dom/base/test/test_meta_viewport2.html
+++ b/dom/base/test/test_meta_viewport2.html
@@ -1,77 +1,42 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>meta viewport test</title>
   <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/AddTask.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
   <meta name="viewport" content="width=device-width">
   <script src="viewport_helpers.js"></script>
 </head>
 <body>
   <p>width=device-width</p>
   <script type="application/javascript">
     "use strict";
 
-    SimpleTest.waitForExplicitFinish();
-
-    let tests = [];
+    add_task(async function test1() {
+      await SpecialPowers.pushPrefEnv(scaleRatio(1.0));
 
-    tests.push(function test1() {
-      SpecialPowers.pushPrefEnv(scaleRatio(1.0),
-        function() {
-          let info = getViewportInfo(800, 480);
-          is(info.defaultZoom, 1,    "initial zoom is 100%");
-          is(info.width,       800,  "width is the same as the displayWidth");
-          is(info.height,      480,  "height is the same as the displayHeight");
-          is(info.autoSize,    true, "width=device-width enables autoSize");
-          is(info.allowZoom,   true, "zooming is enabled by default");
+      let info = getViewportInfo(800, 480);
+      is(info.defaultZoom, 1,    "initial zoom is 100%");
+      is(info.width,       800,  "width is the same as the displayWidth");
+      is(info.height,      480,  "height is the same as the displayHeight");
+      is(info.autoSize,    true, "width=device-width enables autoSize");
+      is(info.allowZoom,   true, "zooming is enabled by default");
 
-          info = getViewportInfo(900, 600);
-          is(info.width,       900,  "changing the displayWidth changes the width");
-          is(info.height,      600,  "changing the displayHeight changes the height");
-
-          nextTest();
-        });
+      info = getViewportInfo(900, 600);
+      is(info.width,       900,  "changing the displayWidth changes the width");
+      is(info.height,      600,  "changing the displayHeight changes the height");
     });
 
-    tests.push(function test2() {
-      SpecialPowers.pushPrefEnv(scaleRatio(1.5),
-        function() {
-          let info = getViewportInfo(900, 600);
-          is(info.defaultZoom, 1.5,  "initial zoom is 150%");
-          is(info.width,       600,  "width equals displayWidth/1.5");
-          is(info.height,      400,  "height equals displayHeight/1.5");
-
-          nextTest();
-        });
-    });
-
-    function getViewportInfo(aDisplayWidth, aDisplayHeight) {
-      let defaultZoom = {}, allowZoom = {}, minZoom = {}, maxZoom = {},
-          width = {}, height = {}, autoSize = {};
+    add_task(async function test2() {
+      await SpecialPowers.pushPrefEnv(scaleRatio(1.5));
 
-      let cwu = SpecialPowers.getDOMWindowUtils(window);
-      cwu.getViewportInfo(aDisplayWidth, aDisplayHeight, defaultZoom, allowZoom,
-                          minZoom, maxZoom, width, height, autoSize);
-      return {
-        defaultZoom: defaultZoom.value,
-        minZoom: minZoom.value,
-        maxZoom: maxZoom.value,
-        width: width.value,
-        height: height.value,
-        autoSize: autoSize.value,
-        allowZoom: allowZoom.value
-      };
-    }
-
-    function nextTest() {
-      if (tests.length)
-        (tests.shift())();
-      else
-        SimpleTest.finish();
-    }
-    addEventListener("load", nextTest);
+      let info = getViewportInfo(900, 600);
+      is(info.defaultZoom, 1.5,  "initial zoom is 150%");
+      is(info.width,       600,  "width equals displayWidth/1.5");
+      is(info.height,      400,  "height equals displayHeight/1.5");
+    });
   </script>
 </body>
 </html>
--- a/dom/base/test/test_meta_viewport3.html
+++ b/dom/base/test/test_meta_viewport3.html
@@ -1,79 +1,44 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>meta viewport test</title>
   <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/AddTask.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
   <meta name="viewport" content="width=320">
   <script src="viewport_helpers.js"></script>
 </head>
 <body>
   <p>width=320</p>
   <script type="application/javascript">
     "use strict";
 
-    SimpleTest.waitForExplicitFinish();
-
-    let tests = [];
+    add_task(async function test1() {
+      await SpecialPowers.pushPrefEnv(scaleRatio(1.0));
 
-    tests.push(function test1() {
-      SpecialPowers.pushPrefEnv(scaleRatio(1.0),
-        function() {
-          let info = getViewportInfo(800, 80);
-          is(info.defaultZoom, 2.5,   "initial zoom fits the displayWidth");
-          is(info.width,       320,   "width is set explicitly");
-          is(info.height,      40,   "height is at the absolute minimum");
-          is(info.autoSize,    false, "width=device-width enables autoSize");
-          is(info.allowZoom,   true,  "zooming is enabled by default");
+      let info = getViewportInfo(800, 80);
+      is(info.defaultZoom, 2.5,   "initial zoom fits the displayWidth");
+      is(info.width,       320,   "width is set explicitly");
+      is(info.height,      40,   "height is at the absolute minimum");
+      is(info.autoSize,    false, "width=device-width enables autoSize");
+      is(info.allowZoom,   true,  "zooming is enabled by default");
 
-          info = getViewportInfo(480, 800);
-          is(info.defaultZoom, 1.5,   "initial zoom fits the new displayWidth");
-          is(info.width,       320,   "explicit width is unchanged");
-          is(info.height,      533,   "height changes proportional to displayHeight");
-
-          nextTest();
-        });
+      info = getViewportInfo(480, 800);
+      is(info.defaultZoom, 1.5,   "initial zoom fits the new displayWidth");
+      is(info.width,       320,   "explicit width is unchanged");
+      is(info.height,      533,   "height changes proportional to displayHeight");
     });
 
-    tests.push(function test2() {
-      SpecialPowers.pushPrefEnv(scaleRatio(1.5),
-        function() {
-          // With an explicit width in CSS px, the scaleRatio has no effect.
-          let info = getViewportInfo(800, 80);
-          is(info.defaultZoom, 2.5,   "initial zoom still fits the displayWidth");
-          is(info.width,       320,   "width is still set explicitly");
-          is(info.height,      40,   "height is still minimum height");
-
-          nextTest();
-        });
-    });
-
-    function getViewportInfo(aDisplayWidth, aDisplayHeight) {
-      let defaultZoom = {}, allowZoom = {}, minZoom = {}, maxZoom = {},
-          width = {}, height = {}, autoSize = {};
+    add_task(async function test2() {
+      await SpecialPowers.pushPrefEnv(scaleRatio(1.5));
 
-      let cwu = SpecialPowers.getDOMWindowUtils(window);
-      cwu.getViewportInfo(aDisplayWidth, aDisplayHeight, defaultZoom, allowZoom,
-                          minZoom, maxZoom, width, height, autoSize);
-      return {
-        defaultZoom: defaultZoom.value,
-        minZoom: minZoom.value,
-        maxZoom: maxZoom.value,
-        width: width.value,
-        height: height.value,
-        autoSize: autoSize.value,
-        allowZoom: allowZoom.value
-      };
-    }
-
-    function nextTest() {
-      if (tests.length)
-        (tests.shift())();
-      else
-        SimpleTest.finish();
-    }
-    addEventListener("load", nextTest);
+      // With an explicit width in CSS px, the scaleRatio has no effect.
+      let info = getViewportInfo(800, 80);
+      is(info.defaultZoom, 2.5,   "initial zoom still fits the displayWidth");
+      is(info.width,       320,   "width is still set explicitly");
+      is(info.height,      40,   "height is still minimum height");
+    });
   </script>
 </body>
 </html>
--- a/dom/base/test/test_meta_viewport4.html
+++ b/dom/base/test/test_meta_viewport4.html
@@ -1,78 +1,43 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>meta viewport test</title>
   <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/AddTask.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
   <meta name="viewport" content="initial-scale=1.0, user-scalable=no">
   <script src="viewport_helpers.js"></script>
 </head>
 <body>
   <p>initial-scale=1.0, user-scalable=no</p>
   <script type="application/javascript">
     "use strict";
 
-    SimpleTest.waitForExplicitFinish();
-
-    let tests = [];
+    add_task(async function test1() {
+      await SpecialPowers.pushPrefEnv(scaleRatio(1.0));
 
-    tests.push(function test1() {
-      SpecialPowers.pushPrefEnv(scaleRatio(1.0),
-        function() {
-          let info = getViewportInfo(800, 480);
-          is(info.defaultZoom, 1,     "initial zoom is set explicitly");
-          is(info.width,       800,   "width fits the initial zoom level");
-          is(info.height,      480,   "height fits the initial zoom level");
-          is(info.autoSize,    true,  "initial-scale=1 enables autoSize");
-          is(info.allowZoom,   false, "zooming is explicitly disabled");
+      let info = getViewportInfo(800, 480);
+      is(info.defaultZoom, 1,     "initial zoom is set explicitly");
+      is(info.width,       800,   "width fits the initial zoom level");
+      is(info.height,      480,   "height fits the initial zoom level");
+      is(info.autoSize,    true,  "initial-scale=1 enables autoSize");
+      is(info.allowZoom,   false, "zooming is explicitly disabled");
 
-          info = getViewportInfo(480, 800);
-          is(info.defaultZoom, 1,     "initial zoom is still set explicitly");
-          is(info.width,       480,   "width changes to match the displayWidth");
-          is(info.height,      800,   "height changes to match the displayHeight");
-
-          nextTest();
-        });
+      info = getViewportInfo(480, 800);
+      is(info.defaultZoom, 1,     "initial zoom is still set explicitly");
+      is(info.width,       480,   "width changes to match the displayWidth");
+      is(info.height,      800,   "height changes to match the displayHeight");
     });
 
-    tests.push(function test2() {
-      SpecialPowers.pushPrefEnv(scaleRatio(1.5),
-        function() {
-          let info = getViewportInfo(800, 480);
-          is(info.defaultZoom, 1.5,   "initial zoom is adjusted for device pixel ratio");
-          is(info.width,       533,   "width fits the initial zoom");
-          is(info.height,      320,   "height fits the initial zoom");
-
-          nextTest();
-        });
-    });
-
-    function getViewportInfo(aDisplayWidth, aDisplayHeight) {
-      let defaultZoom = {}, allowZoom = {}, minZoom = {}, maxZoom = {},
-          width = {}, height = {}, autoSize = {};
+    add_task(async function test2() {
+      await SpecialPowers.pushPrefEnv(scaleRatio(1.5));
 
-      let cwu = SpecialPowers.getDOMWindowUtils(window);
-      cwu.getViewportInfo(aDisplayWidth, aDisplayHeight, defaultZoom, allowZoom,
-                          minZoom, maxZoom, width, height, autoSize);
-      return {
-        defaultZoom: defaultZoom.value,
-        minZoom: minZoom.value,
-        maxZoom: maxZoom.value,
-        width: width.value,
-        height: height.value,
-        autoSize: autoSize.value,
-        allowZoom: allowZoom.value
-      };
-    }
-
-    function nextTest() {
-      if (tests.length)
-        (tests.shift())();
-      else
-        SimpleTest.finish();
-    }
-    addEventListener("load", nextTest);
+      let info = getViewportInfo(800, 480);
+      is(info.defaultZoom, 1.5,   "initial zoom is adjusted for device pixel ratio");
+      is(info.width,       533,   "width fits the initial zoom");
+      is(info.height,      320,   "height fits the initial zoom");
+    });
   </script>
 </body>
 </html>
--- a/dom/base/test/test_meta_viewport5.html
+++ b/dom/base/test/test_meta_viewport5.html
@@ -1,54 +1,25 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>meta viewport test</title>
   <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/AddTask.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
   <meta name="viewport" content="user-scalable=NO">
   <script src="viewport_helpers.js"></script>
 </head>
 <body>
   <p>user-scalable=NO</p>
   <script type="application/javascript">
     "use strict";
 
-    SimpleTest.waitForExplicitFinish();
+    add_task(async function test1() {
+      await SpecialPowers.pushPrefEnv(scaleRatio(1.0));
 
-    let tests = [];
-
-    tests.push(function test1() {
       let info = getViewportInfo(800, 480);
       is(info.allowZoom, true, "user-scalable values are case-sensitive; 'NO' is not valid");
-
-      nextTest();
     });
-
-    function getViewportInfo(aDisplayWidth, aDisplayHeight) {
-      let defaultZoom = {}, allowZoom = {}, minZoom = {}, maxZoom = {},
-          width = {}, height = {}, autoSize = {};
-
-      let cwu = SpecialPowers.getDOMWindowUtils(window);
-      cwu.getViewportInfo(aDisplayWidth, aDisplayHeight, defaultZoom, allowZoom,
-                          minZoom, maxZoom, width, height, autoSize);
-      return {
-        defaultZoom: defaultZoom.value,
-        minZoom: minZoom.value,
-        maxZoom: maxZoom.value,
-        width: width.value,
-        height: height.value,
-        autoSize: autoSize.value,
-        allowZoom: allowZoom.value
-      };
-    }
-
-    function nextTest() {
-      if (tests.length)
-        (tests.shift())();
-      else
-        SimpleTest.finish();
-    }
-    addEventListener("load", nextTest);
   </script>
 </body>
 </html>
--- a/dom/base/test/test_meta_viewport6.html
+++ b/dom/base/test/test_meta_viewport6.html
@@ -1,83 +1,48 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>meta viewport test</title>
   <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/AddTask.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
   <meta name="viewport" content="width=2000, minimum-scale=0.75">
   <script src="viewport_helpers.js"></script>
 </head>
 <body>
   <p>width=2000, minimum-scale=0.75</p>
   <script type="application/javascript">
     "use strict";
 
-    SimpleTest.waitForExplicitFinish();
-
-    let tests = [];
+    add_task(async function test1() {
+      await SpecialPowers.pushPrefEnv(scaleRatio(1.0));
 
-    tests.push(function test1() {
-      SpecialPowers.pushPrefEnv(scaleRatio(1.0),
-        function() {
-          let info = getViewportInfo(800, 480);
-          is(info.minZoom,     0.75,    "minumum scale is set explicitly");
-          is(info.defaultZoom, 0.75,    "initial scale is bounded by the minimum scale");
-          is(info.maxZoom,     10,      "maximum scale defaults to the absolute maximum");
-          is(info.width,       2000,    "width is set explicitly");
-          is(info.height,      1200,    "height is proportional to displayHeight");
-          is(info.autoSize,    false,   "autoSize is disabled by default");
-          is(info.allowZoom,   true,    "zooming is enabled by default");
+      let info = getViewportInfo(800, 480);
+      is(info.minZoom,     0.75,    "minumum scale is set explicitly");
+      is(info.defaultZoom, 0.75,    "initial scale is bounded by the minimum scale");
+      is(info.maxZoom,     10,      "maximum scale defaults to the absolute maximum");
+      is(info.width,       2000,    "width is set explicitly");
+      is(info.height,      1200,    "height is proportional to displayHeight");
+      is(info.autoSize,    false,   "autoSize is disabled by default");
+      is(info.allowZoom,   true,    "zooming is enabled by default");
 
-          info = getViewportInfo(2000, 1000);
-          is(info.minZoom,     0.75,    "minumum scale is still set explicitly");
-          is(info.defaultZoom, 1,       "initial scale fits the width");
-          is(info.width,       2000,    "width is set explicitly");
-          is(info.height,      1000,    "height is proportional to the new displayHeight");
-
-          nextTest();
-        });
+      info = getViewportInfo(2000, 1000);
+      is(info.minZoom,     0.75,    "minumum scale is still set explicitly");
+      is(info.defaultZoom, 1,       "initial scale fits the width");
+      is(info.width,       2000,    "width is set explicitly");
+      is(info.height,      1000,    "height is proportional to the new displayHeight");
     });
 
-    tests.push(function test2() {
-      SpecialPowers.pushPrefEnv(scaleRatio(1.5),
-        function() {
-          let info = getViewportInfo(800, 480);
-          is(info.minZoom,     1.125,   "minumum scale is converted to device pixel scale");
-          is(info.defaultZoom, 1.125,   "initial scale is bounded by the minimum scale");
-          is(info.maxZoom,     15,      "maximum scale defaults to the absolute maximum");
-          is(info.width,       2000,    "width is still set explicitly");
-          is(info.height,      1200,    "height is still proportional to displayHeight");
-
-          nextTest();
-        });
-    });
-
-    function getViewportInfo(aDisplayWidth, aDisplayHeight) {
-      let defaultZoom = {}, allowZoom = {}, minZoom = {}, maxZoom = {},
-          width = {}, height = {}, autoSize = {};
+    add_task(async function test2() {
+      await SpecialPowers.pushPrefEnv(scaleRatio(1.5));
 
-      let cwu = SpecialPowers.getDOMWindowUtils(window);
-      cwu.getViewportInfo(aDisplayWidth, aDisplayHeight, defaultZoom, allowZoom,
-                          minZoom, maxZoom, width, height, autoSize);
-      return {
-        defaultZoom: defaultZoom.value,
-        minZoom: minZoom.value,
-        maxZoom: maxZoom.value,
-        width: width.value,
-        height: height.value,
-        autoSize: autoSize.value,
-        allowZoom: allowZoom.value
-      };
-    }
-
-    function nextTest() {
-      if (tests.length)
-        (tests.shift())();
-      else
-        SimpleTest.finish();
-    }
-    addEventListener("load", nextTest);
+      let info = getViewportInfo(800, 480);
+      is(info.minZoom,     1.125,   "minumum scale is converted to device pixel scale");
+      is(info.defaultZoom, 1.125,   "initial scale is bounded by the minimum scale");
+      is(info.maxZoom,     15,      "maximum scale defaults to the absolute maximum");
+      is(info.width,       2000,    "width is still set explicitly");
+      is(info.height,      1200,    "height is still proportional to displayHeight");
+    });
   </script>
 </body>
 </html>
--- a/dom/base/test/test_meta_viewport7.html
+++ b/dom/base/test/test_meta_viewport7.html
@@ -1,114 +1,72 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>meta viewport test</title>
   <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/AddTask.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
   <meta name="viewport" content="width=320">
   <script src="viewport_helpers.js"></script>
 </head>
 <body>
   <p>Dynamic viewport updates</p>
   <script type="application/javascript">
     "use strict";
 
-    SimpleTest.waitForExplicitFinish();
-
-    let tests = [];
+    add_task(async function test1() {
+      await SpecialPowers.pushPrefEnv(scaleRatio(1.0));
 
-    tests.push(function test1() {
-      SpecialPowers.pushPrefEnv(scaleRatio(1.0),
-        function() {
-          updateViewport("width=device-width");
-          let info = getViewportInfo(800, 480);
-          is(info.defaultZoom, 1,    "initial zoom is 100%");
-          is(info.width,       800,  "width is the same as the displayWidth");
-          is(info.height,      480,  "height is the same as the displayHeight");
-          is(info.autoSize,    true, "width=device-width enables autoSize");
-          is(info.allowZoom,   true, "zooming is enabled by default");
+      updateViewport("width=device-width");
+      let info = getViewportInfo(800, 480);
+      is(info.defaultZoom, 1,    "initial zoom is 100%");
+      is(info.width,       800,  "width is the same as the displayWidth");
+      is(info.height,      480,  "height is the same as the displayHeight");
+      is(info.autoSize,    true, "width=device-width enables autoSize");
+      is(info.allowZoom,   true, "zooming is enabled by default");
 
-          info = getViewportInfo(900, 600);
-          is(info.width,       900,  "changing the displayWidth changes the width");
-          is(info.height,      600,  "changing the displayHeight changes the height");
-
-          nextTest();
-        });
+      info = getViewportInfo(900, 600);
+      is(info.width,       900,  "changing the displayWidth changes the width");
+      is(info.height,      600,  "changing the displayHeight changes the height");
     });
 
-    tests.push(function test2() {
-      SpecialPowers.pushPrefEnv(scaleRatio(1.0),
-        function() {
-          updateViewport("width=320");
-          let info = getViewportInfo(800, 80);
-          is(info.defaultZoom, 2.5,   "initial zoom fits the displayWidth");
-          is(info.width,       320,   "width is set explicitly");
-          is(info.height,      40,   "height is at the absolute minimum");
-          is(info.autoSize,    false, "width=device-width enables autoSize");
-          is(info.allowZoom,   true,  "zooming is enabled by default");
+    add_task(async function test2() {
+      await SpecialPowers.pushPrefEnv(scaleRatio(1.0));
 
-          info = getViewportInfo(480, 800);
-          is(info.defaultZoom, 1.5,   "initial zoom fits the new displayWidth");
-          is(info.width,       320,   "explicit width is unchanged");
-          is(info.height,      533,   "height changes proportional to displayHeight");
+      updateViewport("width=320");
+      let info = getViewportInfo(800, 80);
+      is(info.defaultZoom, 2.5,   "initial zoom fits the displayWidth");
+      is(info.width,       320,   "width is set explicitly");
+      is(info.height,      40,   "height is at the absolute minimum");
+      is(info.autoSize,    false, "width=device-width enables autoSize");
+      is(info.allowZoom,   true,  "zooming is enabled by default");
 
-          nextTest();
-        });
+      info = getViewportInfo(480, 800);
+      is(info.defaultZoom, 1.5,   "initial zoom fits the new displayWidth");
+      is(info.width,       320,   "explicit width is unchanged");
+      is(info.height,      533,   "height changes proportional to displayHeight");
     });
 
-    tests.push(function test3() {
-      SpecialPowers.pushPrefEnv(scaleRatio(1.0),
-        function() {
-          updateViewport("user-scalable=no");
-          let info = getViewportInfo(800, 480);
-          is(info.allowZoom,   false, "zooming is explicitly disabled");
+    add_task(async function test3() {
+      await SpecialPowers.pushPrefEnv(scaleRatio(1.0));
 
-          nextTest();
-        });
+      updateViewport("user-scalable=no");
+      let info = getViewportInfo(800, 480);
+      is(info.allowZoom,   false, "zooming is explicitly disabled");
     });
 
-    tests.push(function test4() {
-      SpecialPowers.pushPrefEnv(scaleRatio(1.0),
-        function() {
-          updateViewport("user-scalable=yes");
-          let info = getViewportInfo(800, 480);
-          is(info.allowZoom,   true,  "zooming is explicitly allowed");
-
-          nextTest();
-        });
-    });
+    add_task(async function test4() {
+      await SpecialPowers.pushPrefEnv(scaleRatio(1.0));
 
-    function getViewportInfo(aDisplayWidth, aDisplayHeight) {
-      let defaultZoom = {}, allowZoom = {}, minZoom = {}, maxZoom = {},
-          width = {}, height = {}, autoSize = {};
-
-      let cwu = SpecialPowers.getDOMWindowUtils(window);
-      cwu.getViewportInfo(aDisplayWidth, aDisplayHeight, defaultZoom, allowZoom,
-                          minZoom, maxZoom, width, height, autoSize);
-      return {
-        defaultZoom: defaultZoom.value,
-        minZoom: minZoom.value,
-        maxZoom: maxZoom.value,
-        width: width.value,
-        height: height.value,
-        autoSize: autoSize.value,
-        allowZoom: allowZoom.value
-      };
-    }
+      updateViewport("user-scalable=yes");
+      let info = getViewportInfo(800, 480);
+      is(info.allowZoom,   true,  "zooming is explicitly allowed");
+    });
 
     function updateViewport(content) {
       let meta = document.querySelector("meta[name=viewport]");
       meta.content = content;
     }
-
-    function nextTest() {
-      if (tests.length) {
-        (tests.shift())();
-      } else {
-        SimpleTest.finish();
-      }
-    }
-    addEventListener("load", nextTest);
   </script>
 </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/dom/base/test/test_meta_viewport_maximum_scale_0.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>meta viewport test</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/AddTask.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <meta name="viewport" content="width=device-width, minimum-scale=1, maximum-scale=0">
+  <script src="viewport_helpers.js"></script>
+</head>
+<body>
+  <p>width=device-width, minimum-scale=1, maximum-scale=0</p>
+  <script type="application/javascript">
+    "use strict";
+
+    add_task(async function test1() {
+      await SpecialPowers.pushPrefEnv(scaleRatio(1.0));
+
+      let info = getViewportInfo(800, 480);
+      is(info.maxZoom,     1,   "maximum_scale should be clamped");
+      is(info.defaultZoom, 1,   "initial scale should be 1");
+      is(info.minZoom,     1,   "minimum scale should be 1");
+      is(info.width,       800, "width should be 800");
+      is(info.height,      480, "height should be 480");
+    });
+  </script>
+</body>
+</html>
--- a/dom/base/test/viewport_helpers.js
+++ b/dom/base/test/viewport_helpers.js
@@ -1,3 +1,27 @@
 function scaleRatio(scale) {
-  return {"set": [["layout.css.devPixelsPerPx", "" + scale]]};
+  return {
+    "set": [
+      [ "layout.css.devPixelsPerPx", "" + scale ],
+      [ "apz.allow_zooming", true ],
+      [ "dom.meta-viewport.enabled", true ],
+    ]
+  };
 }
+
+function getViewportInfo(aDisplayWidth, aDisplayHeight) {
+  let defaultZoom = {}, allowZoom = {}, minZoom = {}, maxZoom = {},
+      width = {}, height = {}, autoSize = {};
+
+  let cwu = SpecialPowers.getDOMWindowUtils(window);
+  cwu.getViewportInfo(aDisplayWidth, aDisplayHeight, defaultZoom, allowZoom,
+                      minZoom, maxZoom, width, height, autoSize);
+  return {
+    defaultZoom: defaultZoom.value,
+    minZoom: minZoom.value,
+    maxZoom: maxZoom.value,
+    width: width.value,
+    height: height.value,
+    autoSize: autoSize.value,
+    allowZoom: allowZoom.value
+  };
+}
--- a/dom/interfaces/base/nsIDOMWindowUtils.idl
+++ b/dom/interfaces/base/nsIDOMWindowUtils.idl
@@ -110,17 +110,17 @@ interface nsIDOMWindowUtils : nsISupport
 
   /**
    * Get the last used layer transaction id for this window's refresh driver.
    */
   readonly attribute unsigned long long lastTransactionId;
 
   /**
    * Information retrieved from the <meta name="viewport"> tag.
-   * See nsContentUtils::GetViewportInfo for more information.
+   * See nsIDocument::GetViewportInfo for more information.
    */
   void getViewportInfo(in uint32_t aDisplayWidth, in uint32_t aDisplayHeight,
                        out double aDefaultZoom, out boolean aAllowZoom,
                        out double aMinZoom, out double aMaxZoom,
                        out uint32_t aWidth, out uint32_t aHeight,
                        out boolean aAutoSize);
 
   /**
--- a/editor/libeditor/HTMLEditor.h
+++ b/editor/libeditor/HTMLEditor.h
@@ -1120,16 +1120,17 @@ protected: // Shouldn't be used by frien
    *                            there is no next <tr> element, this returns
    *                            nullptr but does not return error.
    */
   Element*
   GetNextTableRowElement(Element& aTableRowElement,
                          ErrorResult& aRv) const;
 
   struct CellAndIndexes;
+  struct CellData;
 
   /**
    * CellIndexes store both row index and column index of a table cell.
    */
   struct MOZ_STACK_CLASS CellIndexes final
   {
     int32_t mRow;
     int32_t mColumn;
@@ -1179,24 +1180,34 @@ protected: // Shouldn't be used by frien
      * Update mRowIndex and mColumnIndex with indexes of cell element which
      * contains anchor of Selection.
      *
      * @param                   See above.
      */
     void Update(HTMLEditor& aHTMLEditor, Selection& aSelection,
                 ErrorResult& aRv);
 
+    bool operator==(const CellIndexes& aOther) const
+    {
+      return mRow == aOther.mRow && mColumn == aOther.mColumn;
+    }
+    bool operator!=(const CellIndexes& aOther) const
+    {
+      return mRow != aOther.mRow || mColumn != aOther.mColumn;
+    }
+
   private:
     CellIndexes()
       : mRow(-1)
       , mColumn(-1)
     {
     }
 
     friend struct CellAndIndexes;
+    friend struct CellData;
   };
 
   struct MOZ_STACK_CLASS CellAndIndexes final
   {
     RefPtr<Element> mElement;
     CellIndexes mIndexes;
 
     /**
@@ -1216,16 +1227,203 @@ protected: // Shouldn't be used by frien
      * first range of the Selection.  Note that even if the first range is
      * in the cell element, this does not treat it as the cell element is
      * selected.
      */
     void Update(HTMLEditor& aHTMLEditor, Selection& aSelection,
                 ErrorResult& aRv);
   };
 
+  struct MOZ_STACK_CLASS CellData final
+  {
+    RefPtr<Element> mElement;
+    // Current indexes which this is initialized with.
+    CellIndexes mCurrent;
+    // First column/row indexes of the cell.  When current position is spanned
+    // from other column/row, this value becomes different from mCurrent.
+    CellIndexes mFirst;
+    // Computed rowspan/colspan values which are specified to the cell.
+    // Note that if the cell has larger rowspan/colspan value than actual
+    // table size, these values are the larger values.
+    int32_t mRowSpan;
+    int32_t mColSpan;
+    // Effective rowspan/colspan value at the index.  For example, if first
+    // cell element in first row has rowspan="3", then, if this is initialized
+    // with 0-0 indexes, effective rowspan is 3.  However, if this is
+    // initialized with 1-0 indexes, effective rowspan is 2.
+    int32_t mEffectiveRowSpan;
+    int32_t mEffectiveColSpan;
+    // mIsSelected is set to true if mElement itself or its parent <tr> or
+    // <table> is selected.  Otherwise, e.g., the cell just contains selection
+    // range, this is set to false.
+    bool mIsSelected;
+
+    CellData()
+      : mRowSpan(-1)
+      , mColSpan(-1)
+      , mEffectiveRowSpan(-1)
+      , mEffectiveColSpan(-1)
+      , mIsSelected(false)
+    {
+    }
+
+    /**
+     * Those constructors initializes the members with a <table> element and
+     * both row and column index to specify a cell element.
+     */
+    CellData(HTMLEditor& aHTMLEditor,
+             Element& aTableElement,
+             int32_t aRowIndex,
+             int32_t aColumnIndex,
+             ErrorResult& aRv)
+    {
+      Update(aHTMLEditor, aTableElement, aRowIndex, aColumnIndex, aRv);
+    }
+
+    CellData(HTMLEditor& aHTMLEditor,
+             Element& aTableElement,
+             const CellIndexes& aIndexes,
+             ErrorResult& aRv)
+    {
+      Update(aHTMLEditor, aTableElement, aIndexes, aRv);
+    }
+
+    /**
+     * Those Update() methods updates the members with a <table> element and
+     * both row and column index to specify a cell element.
+     */
+    void Update(HTMLEditor& aHTMLEditor,
+                Element& aTableElement,
+                int32_t aRowIndex,
+                int32_t aColumnIndex,
+                ErrorResult& aRv)
+    {
+      mCurrent.mRow = aRowIndex;
+      mCurrent.mColumn = aColumnIndex;
+      Update(aHTMLEditor, aTableElement, aRv);
+    }
+
+    void Update(HTMLEditor& aHTMLEditor,
+                Element& aTableElement,
+                const CellIndexes& aIndexes,
+                ErrorResult& aRv)
+    {
+      mCurrent = aIndexes;
+      Update(aHTMLEditor, aTableElement, aRv);
+    }
+
+    void Update(HTMLEditor& aHTMLEditor,
+                Element& aTableElement,
+                ErrorResult& aRv);
+
+    /**
+     * FailedOrNotFound() returns true if this failed to initialize/update
+     * or succeeded but found no cell element.
+     */
+    bool FailedOrNotFound() const { return !mElement; }
+
+    /**
+     * IsSpannedFromOtherRowOrColumn(), IsSpannedFromOtherColumn and
+     * IsSpannedFromOtherRow() return true if there is no cell element
+     * at the index because of spanning from other row and/or column.
+     */
+    bool IsSpannedFromOtherRowOrColumn() const
+    {
+      return mElement && mCurrent != mFirst;
+    }
+    bool IsSpannedFromOtherColumn() const
+    {
+      return mElement && mCurrent.mColumn != mFirst.mColumn;
+    }
+    bool IsSpannedFromOtherRow() const
+    {
+      return mElement && mCurrent.mRow != mFirst.mRow;
+    }
+
+    /**
+     * NextColumnIndex() and NextRowIndex() return column/row index of
+     * next cell.  Note that this does not check whether there is next
+     * cell or not actually.
+     */
+    int32_t NextColumnIndex() const
+    {
+      if (NS_WARN_IF(FailedOrNotFound())) {
+        return -1;
+      }
+      return mCurrent.mColumn + mEffectiveColSpan;
+    }
+    int32_t NextRowIndex() const
+    {
+      if (NS_WARN_IF(FailedOrNotFound())) {
+        return -1;
+      }
+      return mCurrent.mRow + mEffectiveRowSpan;
+    }
+
+    /**
+     * LastColumnIndex() and LastRowIndex() return column/row index of
+     * column/row which is spanned by the cell.
+     */
+    int32_t LastColumnIndex() const
+    {
+      if (NS_WARN_IF(FailedOrNotFound())) {
+        return -1;
+      }
+      return NextColumnIndex() - 1;
+    }
+    int32_t LastRowIndex() const
+    {
+      if (NS_WARN_IF(FailedOrNotFound())) {
+        return -1;
+      }
+      return NextRowIndex() - 1;
+    }
+
+    /**
+     * NumberOfPrecedingColmuns() and NumberOfPrecedingRows() return number of
+     * preceding columns/rows if current index is spanned from other column/row.
+     * Otherwise, i.e., current point is not spanned form other column/row,
+     * returns 0.
+     */
+    int32_t NumberOfPrecedingColmuns() const
+    {
+      if (NS_WARN_IF(FailedOrNotFound())) {
+        return -1;
+      }
+      return mCurrent.mColumn - mFirst.mColumn;
+    }
+    int32_t NumberOfPrecedingRows() const
+    {
+      if (NS_WARN_IF(FailedOrNotFound())) {
+        return -1;
+      }
+      return mCurrent.mRow - mFirst.mRow;
+    }
+
+    /**
+     * NumberOfFollowingColumns() and NumberOfFollowingRows() return
+     * number of remaining columns/rows if the cell spans to other
+     * column/row.
+     */
+    int32_t NumberOfFollowingColumns() const
+    {
+      if (NS_WARN_IF(FailedOrNotFound())) {
+        return -1;
+      }
+      return mEffectiveColSpan - 1;
+    }
+    int32_t NumberOfFollowingRows() const
+    {
+      if (NS_WARN_IF(FailedOrNotFound())) {
+        return -1;
+      }
+      return mEffectiveRowSpan - 1;
+    }
+  };
+
   /**
    * TableSize stores and computes number of rows and columns of a <table>
    * element.
    */
   struct MOZ_STACK_CLASS TableSize final
   {
     int32_t mRowCount;
     int32_t mColumnCount;
--- a/editor/libeditor/HTMLTableEditor.cpp
+++ b/editor/libeditor/HTMLTableEditor.cpp
@@ -191,52 +191,47 @@ HTMLEditor::InsertTableCellsWithTransact
   nsresult rv = GetCellContext(nullptr,
                                getter_AddRefs(table),
                                getter_AddRefs(curCell),
                                getter_AddRefs(cellParent), &cellOffset,
                                &startRowIndex, &startColIndex);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
-  if (NS_WARN_IF(!curCell)) {
+  if (NS_WARN_IF(!table) || NS_WARN_IF(!curCell)) {
     // Don't fail if no cell found.
     return NS_OK;
   }
 
   // Get more data for current cell in row we are inserting at since we need
   // colspan value.
-  int32_t curStartRowIndex = 0, curStartColIndex = 0;
-  int32_t rowSpan = 0, colSpan = 0;
-  int32_t actualRowSpan = 0, actualColSpan = 0;
-  bool isSelected = false;
-  rv = GetCellDataAt(table, startRowIndex, startColIndex,
-                     getter_AddRefs(curCell),
-                     &curStartRowIndex, &curStartColIndex, &rowSpan, &colSpan,
-                     &actualRowSpan, &actualColSpan, &isSelected);
-  if (NS_WARN_IF(NS_FAILED(rv))) {
-    return rv;
-  }
-  if (NS_WARN_IF(!curCell)) {
+  IgnoredErrorResult ignoredError;
+  CellData cellDataAtSelection(*this, *table, startRowIndex, startColIndex,
+                               ignoredError);
+  if (NS_WARN_IF(cellDataAtSelection.FailedOrNotFound())) {
     return NS_ERROR_FAILURE;
   }
+  MOZ_ASSERT(curCell == cellDataAtSelection.mElement);
 
   int32_t newCellIndex;
   switch (aInsertPosition) {
     case InsertPosition::eBeforeSelectedCell:
-      newCellIndex = startColIndex;
+      newCellIndex = cellDataAtSelection.mCurrent.mColumn;
       break;
     case InsertPosition::eAfterSelectedCell:
-      newCellIndex = startColIndex + colSpan;
+      MOZ_ASSERT(!cellDataAtSelection.IsSpannedFromOtherRowOrColumn());
+      newCellIndex = cellDataAtSelection.NextColumnIndex();
       break;
     default:
       MOZ_ASSERT_UNREACHABLE("Invalid InsertPosition");
   }
 
   // We control selection resetting after the insert.
-  AutoSelectionSetterAfterTableEdit setCaret(*this, table, startRowIndex,
+  AutoSelectionSetterAfterTableEdit setCaret(*this, table,
+                                             cellDataAtSelection.mCurrent.mRow,
                                              newCellIndex, ePreviousColumn,
                                              false);
   // So, suppress Rules System selection munging.
   AutoTransactionsConserveSelection dontChangeSelection(*this);
 
   EditorDOMPoint pointToInsert(cellParent, cellOffset);
   if (NS_WARN_IF(!pointToInsert.IsSet())) {
     return NS_ERROR_FAILURE;
@@ -429,37 +424,29 @@ HTMLEditor::InsertTableColumnsWithTransa
   nsresult rv = GetCellContext(getter_AddRefs(selection),
                                getter_AddRefs(table),
                                getter_AddRefs(curCell),
                                nullptr, nullptr,
                                &startRowIndex, &startColIndex);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
-  if (NS_WARN_IF(!curCell)) {
+  if (NS_WARN_IF(!table) || NS_WARN_IF(!curCell)) {
     // Don't fail if no cell found.
     return NS_OK;
   }
 
   // Get more data for current cell, we need rowspan value.
-  int32_t curStartRowIndex = 0, curStartColIndex = 0;
-  int32_t rowSpan = 0, colSpan = 0;
-  int32_t actualRowSpan = 0, actualColSpan = 0;
-  bool isSelected = false;
-  rv = GetCellDataAt(table, startRowIndex, startColIndex,
-                     getter_AddRefs(curCell),
-                     &curStartRowIndex, &curStartColIndex,
-                     &rowSpan, &colSpan,
-                     &actualRowSpan, &actualColSpan, &isSelected);
-  if (NS_WARN_IF(NS_FAILED(rv))) {
-    return rv;
-  }
-  if (NS_WARN_IF(!curCell)) {
+  IgnoredErrorResult ignoredError;
+  CellData cellDataAtSelection(*this, *table, startRowIndex, startColIndex,
+                               ignoredError);
+  if (NS_WARN_IF(cellDataAtSelection.FailedOrNotFound())) {
     return NS_ERROR_FAILURE;
   }
+  MOZ_ASSERT(curCell == cellDataAtSelection.mElement);
 
   ErrorResult error;
   TableSize tableSize(*this, *table, error);
   if (NS_WARN_IF(error.Failed())) {
     return error.StealNSResult();
   }
   // Should not be empty since we've already found a cell.
   MOZ_ASSERT(!tableSize.IsEmpty());
@@ -470,88 +457,84 @@ HTMLEditor::InsertTableColumnsWithTransa
                                       *this, EditSubAction::eInsertNode,
                                       nsIEditor::eNext);
 
   switch (aInsertPosition) {
     case InsertPosition::eBeforeSelectedCell:
       break;
     case InsertPosition::eAfterSelectedCell:
       // Use column after current cell.
-      startColIndex += actualColSpan;
+      startColIndex += cellDataAtSelection.mEffectiveColSpan;
 
       // Detect when user is adding after a colspan=0 case.
       // Assume they want to stop the "0" behavior and really add a new column.
       // Thus we set the colspan to its true value.
-      if (!colSpan) {
-        SetColSpan(curCell, actualColSpan);
+      if (!cellDataAtSelection.mColSpan) {
+        SetColSpan(cellDataAtSelection.mElement,
+                   cellDataAtSelection.mEffectiveColSpan);
       }
       break;
     default:
       MOZ_ASSERT_UNREACHABLE("Invalid InsertPosition");
   }
 
   // We control selection resetting after the insert.
-  AutoSelectionSetterAfterTableEdit setCaret(*this, table, startRowIndex,
+  AutoSelectionSetterAfterTableEdit setCaret(*this, table,
+                                             cellDataAtSelection.mCurrent.mRow,
                                              startColIndex, ePreviousRow,
                                              false);
   // Suppress Rules System selection munging.
   AutoTransactionsConserveSelection dontChangeSelection(*this);
 
   // If we are inserting after all existing columns, make sure table is
   // "well formed" before appending new column.
   // XXX As far as I've tested, NormalizeTable() always fails to normalize
-  //     non-rectangular table.  So, the following GetCellDataAt() will
-  //     fail if the table is not rectangle.
+  //     non-rectangular table.  So, the following CellData will fail if
+  //     the table is not rectangle.
   if (startColIndex >= tableSize.mColumnCount) {
     if (NS_WARN_IF(!selection)) {
       return NS_ERROR_FAILURE;
     }
     DebugOnly<nsresult> rv = NormalizeTable(*selection, *table);
     NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to normalize the table");
   }
 
   RefPtr<Element> rowElement;
   for (int32_t rowIndex = 0; rowIndex < tableSize.mRowCount; rowIndex++) {
     if (startColIndex < tableSize.mColumnCount) {
       // We are inserting before an existing column.
-      int32_t curStartRowIndex = 0, curStartColIndex = 0;
-      int32_t rowSpan = 0, colSpan = 0;
-      int32_t actualRowSpan = 0, actualColSpan = 0;
-      bool isSelected = false;
-      rv = GetCellDataAt(table, rowIndex, startColIndex,
-                         getter_AddRefs(curCell),
-                         &curStartRowIndex, &curStartColIndex,
-                         &rowSpan, &colSpan,
-                         &actualRowSpan, &actualColSpan, &isSelected);
-      if (NS_WARN_IF(NS_FAILED(rv))) {
-        return rv;
+      CellData cellData(*this, *table, rowIndex, startColIndex, ignoredError);
+      if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+        return NS_ERROR_FAILURE;
       }
 
       // Don't fail entire process if we fail to find a cell (may fail just in
       // particular rows with < adequate cells per row).
-      if (!curCell) {
+      // XXX So, here wants to know whether the CellData actually failed above.
+      //     Fix this later.
+      if (!cellData.mElement) {
         continue;
       }
 
-      if (curStartColIndex < startColIndex) {
+      if (cellData.IsSpannedFromOtherColumn()) {
         // If we have a cell spanning this location, simply increase its
         // colspan to keep table rectangular.
         // Note: we do nothing if colsspan=0, since it should automatically
         // span the new column.
-        if (colSpan > 0) {
-          SetColSpan(curCell, colSpan + aNumberOfColumnsToInsert);
+        if (cellData.mColSpan > 0) {
+          SetColSpan(cellData.mElement,
+                     cellData.mColSpan + aNumberOfColumnsToInsert);
         }
         continue;
       }
 
       // Simply set selection to the current cell. So, we can let
       // InsertTableCellsWithTransaction() do the work.  Insert a new cell
       // before current one.
-      IgnoredErrorResult ignoredError;
-      selection->Collapse(RawRangeBoundary(curCell, 0), ignoredError);
+      selection->Collapse(RawRangeBoundary(cellData.mElement, 0), ignoredError);
       NS_WARNING_ASSERTION(!ignoredError.Failed(),
         "Failed to collapse Selection into the cell");
       rv = InsertTableCellsWithTransaction(aNumberOfColumnsToInsert,
                                            InsertPosition::eBeforeSelectedCell);
       NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to insert a cell element");
       continue;
     }
 
@@ -574,32 +557,30 @@ HTMLEditor::InsertTableColumnsWithTransa
       if (NS_WARN_IF(error.Failed())) {
         return error.StealNSResult();
       }
       if (NS_WARN_IF(!rowElement)) {
         continue;
       }
     }
 
-    nsCOMPtr<nsINode> lastCell;
-    rv = GetLastCellInRow(rowElement, getter_AddRefs(lastCell));
+    nsCOMPtr<nsINode> lastCellNode;
+    rv = GetLastCellInRow(rowElement, getter_AddRefs(lastCellNode));
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
-    if (NS_WARN_IF(!lastCell)) {
+    if (NS_WARN_IF(!lastCellNode)) {
       return NS_ERROR_FAILURE;
     }
 
-    curCell = lastCell->AsElement();
     // Simply add same number of cells to each row.  Although tempted to check
-    // cell indexes for curCell, the effects of colspan > 1 in some cells makes
-    // this futile.  We must use NormalizeTable first to assure that there are
-    // cells in each cellmap location.
-    IgnoredErrorResult ignoredError;
-    selection->Collapse(RawRangeBoundary(curCell, 0), ignoredError);
+    // cell indexes for current cell, the effects of colspan > 1 in some cells
+    // makes this futile.  We must use NormalizeTable first to assure that
+    // there are cells in each cellmap location.
+    selection->Collapse(RawRangeBoundary(lastCellNode, 0), ignoredError);
     NS_WARNING_ASSERTION(!ignoredError.Failed(),
       "Failed to collapse Selection into the cell");
     rv = InsertTableCellsWithTransaction(aNumberOfColumnsToInsert,
                                          InsertPosition::eAfterSelectedCell);
     NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to insert a cell element");
   }
   // XXX This is perhaps the result of the last call of
   //     InsertTableCellsWithTransaction().
@@ -631,38 +612,30 @@ HTMLEditor::InsertTableRowsWithTransacti
   nsresult rv = GetCellContext(nullptr,
                                getter_AddRefs(table),
                                getter_AddRefs(curCell),
                                nullptr, nullptr,
                                &startRowIndex, &startColIndex);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
-  if (NS_WARN_IF(!curCell)) {
+  if (NS_WARN_IF(!table) || NS_WARN_IF(!curCell)) {
     // Don't fail if no cell found.
     return NS_OK;
   }
 
   // Get more data for current cell in row we are inserting at because we need
   // colspan.
-  int32_t curStartRowIndex = 0, curStartColIndex = 0;
-  int32_t rowSpan = 0, colSpan = 0;
-  int32_t actualRowSpan = 0, actualColSpan = 0;
-  bool isSelected = false;
-  rv = GetCellDataAt(table, startRowIndex, startColIndex,
-                     getter_AddRefs(curCell),
-                     &curStartRowIndex, &curStartColIndex,
-                     &rowSpan, &colSpan,
-                     &actualRowSpan, &actualColSpan, &isSelected);
-  if (NS_WARN_IF(NS_FAILED(rv))) {
-    return rv;
-  }
-  if (NS_WARN_IF(!curCell)) {
+  IgnoredErrorResult ignoredError;
+  CellData cellDataAtSelection(*this, *table, startRowIndex, startColIndex,
+                               ignoredError);
+  if (NS_WARN_IF(cellDataAtSelection.FailedOrNotFound())) {
     return NS_ERROR_FAILURE;
   }
+  MOZ_ASSERT(curCell == cellDataAtSelection.mElement);
 
   ErrorResult error;
   TableSize tableSize(*this, *table, error);
   if (NS_WARN_IF(error.Failed())) {
     return error.StealNSResult();
   }
   // Should not be empty since we've already found a cell.
   MOZ_ASSERT(!tableSize.IsEmpty());
@@ -673,112 +646,103 @@ HTMLEditor::InsertTableRowsWithTransacti
                                       *this, EditSubAction::eInsertNode,
                                       nsIEditor::eNext);
 
   switch (aInsertPosition) {
     case InsertPosition::eBeforeSelectedCell:
       break;
     case InsertPosition::eAfterSelectedCell:
       // Use row after current cell.
-      startRowIndex += actualRowSpan;
+      startRowIndex += cellDataAtSelection.mEffectiveRowSpan;
 
       // Detect when user is adding after a rowspan=0 case.
       // Assume they want to stop the "0" behavior and really add a new row.
       // Thus we set the rowspan to its true value.
-      if (!rowSpan) {
-        SetRowSpan(curCell, actualRowSpan);
+      if (!cellDataAtSelection.mRowSpan) {
+        SetRowSpan(cellDataAtSelection.mElement,
+                   cellDataAtSelection.mEffectiveRowSpan);
       }
       break;
     default:
       MOZ_ASSERT_UNREACHABLE("Invalid InsertPosition");
   }
 
   // We control selection resetting after the insert.
-  AutoSelectionSetterAfterTableEdit setCaret(*this, table, startRowIndex,
-                                             startColIndex, ePreviousColumn,
-                                             false);
+  AutoSelectionSetterAfterTableEdit setCaret(
+    *this, table, startRowIndex, cellDataAtSelection.mCurrent.mColumn,
+    ePreviousColumn, false);
   // Suppress Rules System selection munging.
   AutoTransactionsConserveSelection dontChangeSelection(*this);
 
   RefPtr<Element> cellForRowParent;
   int32_t cellsInRow = 0;
   if (startRowIndex < tableSize.mRowCount) {
     // We are inserting above an existing row.  Get each cell in the insert
     // row to adjust for colspan effects while we count how many cells are
     // needed.
-    for (int32_t colIndex = 0, actualColSpan = 0;; colIndex += actualColSpan) {
-      RefPtr<Element> cellElement;
-      int32_t curStartRowIndex = 0, curStartColIndex = 0;
-      int32_t rowSpan = 0, colSpan = 0;
-      int32_t actualRowSpan = 0;
-      nsresult rv = GetCellDataAt(table, startRowIndex, colIndex,
-                                  getter_AddRefs(cellElement),
-                                  &curStartRowIndex, &curStartColIndex,
-                                  &rowSpan, &colSpan,
-                                  &actualRowSpan, &actualColSpan,
-                                  &isSelected);
-      if (NS_FAILED(rv)) {
+    CellData cellData;
+    for (int32_t colIndex = 0;; colIndex = cellData.NextColumnIndex()) {
+      cellData.Update(*this, *table, startRowIndex, colIndex, ignoredError);
+      if (cellData.FailedOrNotFound()) {
         break; // Perhaps, we reach end of the row.
       }
 
-      if (NS_WARN_IF(!cellElement)) {
-        // XXX What's this case?
-        actualColSpan = 1;
-        continue;
+      // XXX So, this is impossible case. Will be removed.
+      if (NS_WARN_IF(!cellData.mElement)) {
+        break;
       }
 
-      if (curStartRowIndex < startRowIndex) {
+      if (cellData.IsSpannedFromOtherRow()) {
         // We have a cell spanning this location.  Increase its rowspan.
         // Note that if rowspan is 0, we do nothing since that cell should
         // automatically extend into the new row.
-        if (rowSpan > 0) {
-          SetRowSpan(cellElement, rowSpan + aNumberOfRowsToInsert);
+        if (cellData.mRowSpan > 0) {
+          SetRowSpan(cellData.mElement,
+                     cellData.mRowSpan + aNumberOfRowsToInsert);
         }
         continue;
       }
 
-      cellsInRow += actualColSpan;
+      cellsInRow += cellData.mEffectiveColSpan;
       if (!cellForRowParent) {
-        cellForRowParent = std::move(cellElement);
+        // FYI: Don't use std::move() here since NextColumnIndex() needs it.
+        cellForRowParent = cellData.mElement;
       }
+
+      MOZ_ASSERT(colIndex < cellData.NextColumnIndex());
     }
   } else {
     // We are adding a new row after all others.  If it weren't for colspan=0
     // effect,  we could simply use tableSize.mColumnCount for number of new
     // cells...
     // XXX colspan=0 support has now been removed in table layout so maybe this
     //     can be cleaned up now? (bug 1243183)
     cellsInRow = tableSize.mColumnCount;
 
     // but we must compensate for all cells with rowspan = 0 in the last row.
     const int32_t kLastRowIndex = tableSize.mRowCount - 1;
-    for (int32_t colIndex = 0, actualColSpan = 0;; colIndex += actualColSpan) {
-      RefPtr<Element> cellElement;
-      int32_t curStartRowIndex = 0, curStartColIndex = 0;
-      int32_t rowSpan = 0, colSpan = 0;
-      int32_t actualRowSpan = 0;
-      nsresult rv = GetCellDataAt(table, kLastRowIndex, colIndex,
-                                  getter_AddRefs(cellElement),
-                                  &curStartRowIndex, &curStartColIndex,
-                                  &rowSpan, &colSpan,
-                                  &actualRowSpan, &actualColSpan,
-                                  &isSelected);
-      if (NS_FAILED(rv)) {
+    CellData cellData;
+    for (int32_t colIndex = 0;; colIndex = cellData.NextColumnIndex()) {
+      cellData.Update(*this, *table, kLastRowIndex, colIndex, ignoredError);
+      if (cellData.FailedOrNotFound()) {
         break; // Perhaps, we reach end of the row.
       }
 
-      if (!rowSpan) {
-        MOZ_ASSERT(cellsInRow >= actualColSpan);
-        cellsInRow -= actualColSpan;
+      if (!cellData.mRowSpan) {
+        MOZ_ASSERT(cellsInRow >= cellData.mEffectiveColSpan);
+        cellsInRow -= cellData.mEffectiveColSpan;
       }
 
       // Save cell from the last row that we will use below
-      if (!cellForRowParent && curStartRowIndex == kLastRowIndex) {
-        cellForRowParent = std::move(cellElement);
+      if (!cellForRowParent && !cellData.IsSpannedFromOtherRow()) {
+        // FYI: Don't use std::move() here since NextColumnIndex() needs it.
+        cellForRowParent = cellData.mElement;
       }
+
+      MOZ_ASSERT(colIndex < cellData.NextColumnIndex());
     }
   }
 
   if (NS_WARN_IF(!cellsInRow)) {
     // There is no cell element in the last row??
     return NS_OK;
   }
 
@@ -1394,80 +1358,73 @@ HTMLEditor::DeleteSelectedTableColumnsWi
 }
 
 nsresult
 HTMLEditor::DeleteTableColumnWithTransaction(Element& aTableElement,
                                              int32_t aColumnIndex)
 {
   // XXX Why don't this method remove proper <col> (and <colgroup>)?
   ErrorResult error;
+  IgnoredErrorResult ignoredError;
   for (int32_t rowIndex = 0;; rowIndex++) {
-    RefPtr<Element> cell;
-    int32_t startRowIndex = 0, startColIndex = 0;
-    int32_t rowSpan = 0, colSpan = 0;
-    int32_t actualRowSpan = 0, actualColSpan = 0;
-    bool isSelected = false;
-    nsresult rv =
-      GetCellDataAt(&aTableElement, rowIndex, aColumnIndex,
-                    getter_AddRefs(cell),
-                    &startRowIndex, &startColIndex, &rowSpan, &colSpan,
-                    &actualRowSpan, &actualColSpan, &isSelected);
+    CellData cellData(*this, aTableElement, rowIndex, aColumnIndex,
+                      ignoredError);
     // Failure means that there is no more row in the table.  In this case,
     // we shouldn't return error since we just reach the end of the table.
-    // XXX Ideally, GetCellDataAt() should return
-    //     NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND in such case instead of
-    //     error.  However, it's used by a lot of methods, so, it's really
-    //     risky to change it.
-    if (NS_FAILED(rv) || !cell) {
+    // XXX Should distinguish whether CellData returns error or just not found
+    //     later.
+    if (cellData.FailedOrNotFound()) {
       return NS_OK;
     }
 
     // Find cells that don't start in column we are deleting.
-    MOZ_ASSERT(colSpan >= 0);
-    if (startColIndex < aColumnIndex || colSpan != 1) {
+    MOZ_ASSERT(cellData.mColSpan >= 0);
+    if (cellData.IsSpannedFromOtherColumn() || cellData.mColSpan != 1) {
       // If we have a cell spanning this location, decrease its colspan to
       // keep table rectangular, but if colspan is 0, it'll be adjusted
       // automatically.
-      if (colSpan > 0) {
-        NS_WARNING_ASSERTION(colSpan > 1, "colspan should be 2 or larger");
-        SetColSpan(cell, colSpan - 1);
+      if (cellData.mColSpan > 0) {
+        NS_WARNING_ASSERTION(cellData.mColSpan > 1,
+                             "colspan should be 2 or larger");
+        SetColSpan(cellData.mElement, cellData.mColSpan - 1);
       }
-      if (startColIndex == aColumnIndex) {
+      if (!cellData.IsSpannedFromOtherColumn()) {
         // Cell is in column to be deleted, but must have colspan > 1,
         // so delete contents of cell instead of cell itself (We must have
         // reset colspan above).
-        DebugOnly<nsresult> rv = DeleteAllChildrenWithTransaction(*cell);
+        DebugOnly<nsresult> rv =
+          DeleteAllChildrenWithTransaction(*cellData.mElement);
         NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
           "Failed to remove all children of the cell element");
       }
       // Skip rows which the removed cell spanned.
-      rowIndex += actualRowSpan - 1;
+      rowIndex += cellData.NumberOfFollowingRows();
       continue;
     }
 
     // Delete the cell
     int32_t numberOfCellsInRow =
-      GetNumberOfCellsInRow(aTableElement, rowIndex);
+      GetNumberOfCellsInRow(aTableElement, cellData.mCurrent.mRow);
     NS_WARNING_ASSERTION(numberOfCellsInRow > 0,
       "Failed to count existing cells in the row");
     if (numberOfCellsInRow != 1) {
       // If removing cell is not the last cell of the row, we can just remove
       // it.
-      rv = DeleteNodeWithTransaction(*cell);
+      nsresult rv = DeleteNodeWithTransaction(*cellData.mElement);
       if (NS_WARN_IF(NS_FAILED(rv))) {
         return rv;
       }
       // Skip rows which the removed cell spanned.
-      rowIndex += actualRowSpan - 1;
+      rowIndex += cellData.NumberOfFollowingRows();
       continue;
     }
 
     // When the cell is the last cell in the row, remove the row instead.
     Element* parentRow =
-      GetElementOrParentByTagNameInternal(*nsGkAtoms::tr, *cell);
+      GetElementOrParentByTagNameInternal(*nsGkAtoms::tr, *cellData.mElement);
     if (NS_WARN_IF(!parentRow)) {
       return NS_ERROR_FAILURE;
     }
 
     // Check if its the only row left in the table.  If so, we can delete
     // the table instead.
     TableSize tableSize(*this, aTableElement, error);
     if (NS_WARN_IF(error.Failed())) {
@@ -1475,27 +1432,28 @@ HTMLEditor::DeleteTableColumnWithTransac
     }
 
     if (tableSize.mRowCount == 1) {
       // We're deleting the last row.  So, let's remove the <table> now.
       RefPtr<Selection> selection = GetSelection();
       if (NS_WARN_IF(!selection)) {
         return NS_ERROR_FAILURE;
       }
-      rv = DeleteTableElementAndChildrenWithTransaction(*selection,
-                                                        aTableElement);
+      nsresult rv =
+        DeleteTableElementAndChildrenWithTransaction(*selection, aTableElement);
       if (NS_WARN_IF(NS_FAILED(rv))) {
         return rv;
       }
       return NS_OK;
     }
 
     // Delete the row by placing caret in cell we were to delete.  We need
     // to call DeleteTableRowWithTransaction() to handle cells with rowspan.
-    rv = DeleteTableRowWithTransaction(aTableElement, startRowIndex);
+    nsresult rv =
+      DeleteTableRowWithTransaction(aTableElement, cellData.mFirst.mRow);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
 
     // Note that we decrement rowIndex since a row was deleted.
     rowIndex--;
   }
 }
@@ -1672,69 +1630,69 @@ HTMLEditor::DeleteTableRowWithTransactio
       : mElement(aSpanCellElement)
       , mNewRowSpanValue(aNewRowSpanValue)
     {
     }
   };
   AutoTArray<SpanCell, 10> spanCellArray;
   RefPtr<Element> cellInDeleteRow;
   int32_t columnIndex = 0;
+  IgnoredErrorResult ignoredError;
   while (aRowIndex < tableSize.mRowCount &&
          columnIndex < tableSize.mColumnCount) {
-    RefPtr<Element> cell;
-    int32_t startRowIndex = 0, startColIndex = 0;
-    int32_t rowSpan = 0, colSpan = 0;
-    int32_t actualRowSpan = 0, actualColSpan = 0;
-    bool isSelected = false;
-    nsresult rv =
-      GetCellDataAt(&aTableElement, aRowIndex, columnIndex,
-                    getter_AddRefs(cell),
-                    &startRowIndex, &startColIndex, &rowSpan, &colSpan,
-                    &actualRowSpan, &actualColSpan, &isSelected);
-    // We don't fail if we don't find a cell, so this must be real bad
-    if (NS_WARN_IF(NS_FAILED(rv))) {
-      return rv;
+    CellData cellData(*this, aTableElement, aRowIndex, columnIndex,
+                      ignoredError);
+    if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+      return NS_ERROR_FAILURE;
     }
 
-    if (!cell) {
+    // XXX So, we should distinguish if CellDate returns error or just not
+    //     found later.
+    if (!cellData.mElement) {
       break;
     }
+
     // Compensate for cells that don't start or extend below the row we are
     // deleting.
-    if (startRowIndex < aRowIndex) {
+    if (cellData.IsSpannedFromOtherRow()) {
       // If a cell starts in row above us, decrease its rowspan to keep table
       // rectangular but we don't need to do this if rowspan=0, since it will
       // be automatically adjusted.
-      if (rowSpan > 0) {
+      if (cellData.mRowSpan > 0) {
         // Build list of cells to change rowspan.  We can't do it now since
         // it upsets cell map, so we will do it after deleting the row.
         int32_t newRowSpanValue =
-          std::max(aRowIndex - startRowIndex, actualRowSpan - 1);
-        spanCellArray.AppendElement(SpanCell(cell, newRowSpanValue));
+          std::max(cellData.NumberOfPrecedingRows(),
+                   cellData.NumberOfFollowingRows());
+        spanCellArray.AppendElement(
+                        SpanCell(cellData.mElement, newRowSpanValue));
       }
     } else {
-      if (rowSpan > 1) {
+      if (cellData.mRowSpan > 1) {
         // Cell spans below row to delete, so we must insert new cells to
         // keep rows below.  Note that we test "rowSpan" so we don't do this
         // if rowSpan = 0 (automatic readjustment).
-        int32_t aboveRowToInsertNewCellInto = aRowIndex - startRowIndex + 1;
-        int32_t numOfRawSpanRemainingBelow = actualRowSpan - 1;
-        rv = SplitCellIntoRows(&aTableElement, startRowIndex, startColIndex,
-                               aboveRowToInsertNewCellInto,
-                               numOfRawSpanRemainingBelow, nullptr);
+        int32_t aboveRowToInsertNewCellInto =
+          cellData.NumberOfPrecedingRows() + 1;
+        nsresult rv =
+          SplitCellIntoRows(&aTableElement,
+                            cellData.mFirst.mRow, cellData.mFirst.mColumn,
+                            aboveRowToInsertNewCellInto,
+                            cellData.NumberOfFollowingRows(), nullptr);
         if (NS_WARN_IF(NS_FAILED(rv))) {
           return rv;
         }
       }
       if (!cellInDeleteRow) {
-        cellInDeleteRow = cell; // Reference cell to find row to delete
+        // Reference cell to find row to delete.
+        cellInDeleteRow = std::move(cellData.mElement);
       }
     }
     // Skip over other columns spanned by this cell
-    columnIndex += actualColSpan;
+    columnIndex += cellData.mEffectiveColSpan;
   }
 
   // Things are messed up if we didn't find a cell in the row!
   if (NS_WARN_IF(!cellInDeleteRow)) {
     return NS_ERROR_FAILURE;
   }
 
   // Delete the entire row.
@@ -1853,17 +1811,16 @@ HTMLEditor::SelectBlockOfCells(Element* 
   int32_t minRow =
     std::min(startCellIndexes.mRow, endCellIndexes.mRow);
   int32_t maxColumn =
     std::max(startCellIndexes.mColumn, endCellIndexes.mColumn);
   int32_t maxRow =
     std::max(startCellIndexes.mRow, endCellIndexes.mRow);
 
   RefPtr<Element> cell;
-  int32_t currentRowIndex, currentColIndex;
   cell = GetFirstSelectedTableCellElement(*selection, error);
   if (NS_WARN_IF(error.Failed())) {
     return error.StealNSResult();
   }
   if (!cell) {
     return NS_OK;
   }
   RefPtr<nsRange> range = selection->GetRangeAt(0);
@@ -1887,41 +1844,42 @@ HTMLEditor::SelectBlockOfCells(Element* 
       return error.StealNSResult();
     }
     if (cell) {
       MOZ_ASSERT(mSelectedCellIndex > 0);
       range = selection->GetRangeAt(mSelectedCellIndex - 1);
     }
   }
 
-  int32_t rowSpan, colSpan, actualRowSpan, actualColSpan;
-  bool    isSelected;
   nsresult rv = NS_OK;
+  IgnoredErrorResult ignoredError;
   for (int32_t row = minRow; row <= maxRow; row++) {
-    for (int32_t col = minColumn; col <= maxColumn;
-        col += std::max(actualColSpan, 1)) {
-      rv = GetCellDataAt(table, row, col, getter_AddRefs(cell),
-                         &currentRowIndex, &currentColIndex,
-                         &rowSpan, &colSpan,
-                         &actualRowSpan, &actualColSpan, &isSelected);
-      if (NS_FAILED(rv)) {
-        break;
+    CellData cellData;
+    for (int32_t col = minColumn;
+         col <= maxColumn;
+         col = cellData.NextColumnIndex()) {
+      cellData.Update(*this, *table, row, col, ignoredError);
+      if (cellData.FailedOrNotFound()) {
+        return NS_ERROR_FAILURE;
       }
+
       // Skip cells that already selected or are spanned from previous locations
-      if (!isSelected && cell &&
-          row == currentRowIndex && col == currentColIndex) {
-        rv = AppendNodeToSelectionAsRange(cell);
+      // XXX So, we should distinguish whether CellData returns error or just
+      //     not found later.
+      if (!cellData.mIsSelected && cellData.mElement &&
+          !cellData.IsSpannedFromOtherRowOrColumn()) {
+        rv = AppendNodeToSelectionAsRange(cellData.mElement);
         if (NS_FAILED(rv)) {
           break;
         }
       }
+      MOZ_ASSERT(col < cellData.NextColumnIndex());
     }
   }
-  // NS_OK, otherwise, the last failure of GetCellDataAt() or
-  // AppendNodeToSelectionAsRange().
+  // NS_OK, otherwise, the last failure of AppendNodeToSelectionAsRange().
   return rv;
 }
 
 NS_IMETHODIMP
 HTMLEditor::SelectAllTableCells()
 {
   RefPtr<Selection> selection = GetSelection();
   if (NS_WARN_IF(!selection)) {
@@ -1954,45 +1912,48 @@ HTMLEditor::SelectAllTableCells()
   SelectionBatcher selectionBatcher(selection);
 
   // It is now safe to clear the selection
   // BE SURE TO RESET IT BEFORE LEAVING!
   nsresult rv = ClearSelection();
 
   // Select all cells in the same column as current cell
   bool cellSelected = false;
-  int32_t rowSpan, colSpan, actualRowSpan, actualColSpan, currentRowIndex, currentColIndex;
-  bool    isSelected;
+  IgnoredErrorResult ignoredError;
   for (int32_t row = 0; row < tableSize.mRowCount; row++) {
+    CellData cellData;
     for (int32_t col = 0;
          col < tableSize.mColumnCount;
-         col += std::max(actualColSpan, 1)) {
-      rv = GetCellDataAt(table, row, col, getter_AddRefs(cell),
-                         &currentRowIndex, &currentColIndex,
-                         &rowSpan, &colSpan,
-                         &actualRowSpan, &actualColSpan, &isSelected);
-      if (NS_FAILED(rv)) {
+         col = cellData.NextColumnIndex()) {
+      cellData.Update(*this, *table, row, col, ignoredError);
+      if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+        rv = NS_ERROR_FAILURE;
         break;
       }
+
       // Skip cells that are spanned from previous rows or columns
-      if (cell && row == currentRowIndex && col == currentColIndex) {
-        rv =  AppendNodeToSelectionAsRange(cell);
+      // XXX So, we should distinguish whether CellData returns error or just
+      //     not found later.
+      if (cellData.mElement &&
+          !cellData.IsSpannedFromOtherRowOrColumn()) {
+        rv =  AppendNodeToSelectionAsRange(cellData.mElement);
         if (NS_FAILED(rv)) {
           break;
         }
         cellSelected = true;
       }
+      MOZ_ASSERT(col < cellData.NextColumnIndex());
     }
   }
   // Safety code to select starting cell if nothing else was selected
   if (!cellSelected) {
     return AppendNodeToSelectionAsRange(startCell);
   }
   // NS_OK, otherwise, the error of ClearSelection() when there is no column or
-  // the last failure of GetCellDataAt() or AppendNodeToSelectionAsRange().
+  // the last failure of CellData or AppendNodeToSelectionAsRange().
   return rv;
 }
 
 NS_IMETHODIMP
 HTMLEditor::SelectTableRow()
 {
   RefPtr<Selection> selection = GetSelection();
   if (NS_WARN_IF(!selection)) {
@@ -2039,42 +2000,46 @@ HTMLEditor::SelectTableRow()
   SelectionBatcher selectionBatcher(selection);
 
   // It is now safe to clear the selection
   // BE SURE TO RESET IT BEFORE LEAVING!
   rv = ClearSelection();
 
   // Select all cells in the same row as current cell
   bool cellSelected = false;
-  int32_t rowSpan, colSpan, actualRowSpan, actualColSpan, currentRowIndex, currentColIndex;
-  bool    isSelected;
+  IgnoredErrorResult ignoredError;
+  CellData cellData;
   for (int32_t col = 0;
        col < tableSize.mColumnCount;
-       col += std::max(actualColSpan, 1)) {
-    rv = GetCellDataAt(table, startRowIndex, col, getter_AddRefs(cell),
-                       &currentRowIndex, &currentColIndex, &rowSpan, &colSpan,
-                       &actualRowSpan, &actualColSpan, &isSelected);
-    if (NS_FAILED(rv)) {
+       col = cellData.NextColumnIndex()) {
+    cellData.Update(*this, *table, startRowIndex, col, ignoredError);
+    if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+      rv = NS_ERROR_FAILURE;
       break;
     }
+
     // Skip cells that are spanned from previous rows or columns
-    if (cell && currentRowIndex == startRowIndex && currentColIndex == col) {
-      rv = AppendNodeToSelectionAsRange(cell);
+    // XXX So, we should distinguish whether CellData returns error or just
+    //     not found later.
+    if (cellData.mElement &&
+        !cellData.IsSpannedFromOtherRowOrColumn()) {
+      rv = AppendNodeToSelectionAsRange(cellData.mElement);
       if (NS_FAILED(rv)) {
         break;
       }
       cellSelected = true;
     }
+    MOZ_ASSERT(col < cellData.NextColumnIndex());
   }
   // Safety code to select starting cell if nothing else was selected
   if (!cellSelected) {
     return AppendNodeToSelectionAsRange(startCell);
   }
   // NS_OK, otherwise, the error of ClearSelection() when there is no column or
-  // the last failure of GetCellDataAt() or AppendNodeToSelectionAsRange().
+  // the last failure of CellData or AppendNodeToSelectionAsRange().
   return rv;
 }
 
 NS_IMETHODIMP
 HTMLEditor::SelectTableColumn()
 {
   RefPtr<Selection> selection = GetSelection();
   if (NS_WARN_IF(!selection)) {
@@ -2117,42 +2082,46 @@ HTMLEditor::SelectTableColumn()
   SelectionBatcher selectionBatcher(selection);
 
   // It is now safe to clear the selection
   // BE SURE TO RESET IT BEFORE LEAVING!
   rv = ClearSelection();
 
   // Select all cells in the same column as current cell
   bool cellSelected = false;
-  int32_t rowSpan, colSpan, actualRowSpan, actualColSpan, currentRowIndex, currentColIndex;
-  bool    isSelected;
+  IgnoredErrorResult ignoredError;
+  CellData cellData;
   for (int32_t row = 0;
        row < tableSize.mRowCount;
-       row += std::max(actualRowSpan, 1)) {
-    rv = GetCellDataAt(table, row, startColIndex, getter_AddRefs(cell),
-                       &currentRowIndex, &currentColIndex, &rowSpan, &colSpan,
-                       &actualRowSpan, &actualColSpan, &isSelected);
-    if (NS_FAILED(rv)) {
+       row = cellData.NextRowIndex()) {
+    cellData.Update(*this, *table, row, startColIndex, ignoredError);
+    if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+      rv = NS_ERROR_FAILURE;
       break;
     }
+
     // Skip cells that are spanned from previous rows or columns
-    if (cell && currentRowIndex == row && currentColIndex == startColIndex) {
-      rv = AppendNodeToSelectionAsRange(cell);
+    // XXX So, we should distinguish whether CellData returns error or just
+    //     not found later.
+    if (cellData.mElement &&
+        !cellData.IsSpannedFromOtherRowOrColumn()) {
+      rv = AppendNodeToSelectionAsRange(cellData.mElement);
       if (NS_FAILED(rv)) {
         break;
       }
       cellSelected = true;
     }
+    MOZ_ASSERT(row < cellData.NextRowIndex());
   }
   // Safety code to select starting cell if nothing else was selected
   if (!cellSelected) {
     return AppendNodeToSelectionAsRange(startCell);
   }
   // NS_OK, otherwise, the error of ClearSelection() when there is no row or
-  // the last failure of GetCellDataAt() or AppendNodeToSelectionAsRange().
+  // the last failure of CellData or AppendNodeToSelectionAsRange().
   return rv;
 }
 
 NS_IMETHODIMP
 HTMLEditor::SplitTableCell()
 {
   RefPtr<Element> table;
   RefPtr<Element> cell;
@@ -2246,54 +2215,61 @@ HTMLEditor::CopyCellBackgroundColor(Elem
 nsresult
 HTMLEditor::SplitCellIntoColumns(Element* aTable,
                                  int32_t aRowIndex,
                                  int32_t aColIndex,
                                  int32_t aColSpanLeft,
                                  int32_t aColSpanRight,
                                  Element** aNewCell)
 {
-  NS_ENSURE_TRUE(aTable, NS_ERROR_NULL_POINTER);
+  if (NS_WARN_IF(!aTable)) {
+    return NS_ERROR_INVALID_ARG;
+  }
   if (aNewCell) {
     *aNewCell = nullptr;
   }
 
-  RefPtr<Element> cell;
-  int32_t startRowIndex, startColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan;
-  bool    isSelected;
-  nsresult rv =
-    GetCellDataAt(aTable, aRowIndex, aColIndex, getter_AddRefs(cell),
-                  &startRowIndex, &startColIndex,
-                  &rowSpan, &colSpan,
-                  &actualRowSpan, &actualColSpan, &isSelected);
-  NS_ENSURE_SUCCESS(rv, rv);
-  NS_ENSURE_TRUE(cell, NS_ERROR_NULL_POINTER);
+  IgnoredErrorResult ignoredError;
+  CellData cellData(*this, *aTable, aRowIndex, aColIndex, ignoredError);
+  if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+    return NS_ERROR_FAILURE;
+  }
 
   // We can't split!
-  if (actualColSpan <= 1 || (aColSpanLeft + aColSpanRight) > actualColSpan) {
+  if (cellData.mEffectiveColSpan <= 1 ||
+      aColSpanLeft + aColSpanRight > cellData.mEffectiveColSpan) {
     return NS_OK;
   }
 
   // Reduce colspan of cell to split
-  rv = SetColSpan(cell, aColSpanLeft);
-  NS_ENSURE_SUCCESS(rv, rv);
+  nsresult rv = SetColSpan(cellData.mElement, aColSpanLeft);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
 
   // Insert new cell after using the remaining span
   //  and always get the new cell so we can copy the background color;
-  RefPtr<Element> newCell;
-  rv = InsertCell(cell, actualRowSpan, aColSpanRight, true, false,
-                  getter_AddRefs(newCell));
-  NS_ENSURE_SUCCESS(rv, rv);
-  if (!newCell) {
+  RefPtr<Element> newCellElement;
+  rv = InsertCell(cellData.mElement,
+                  cellData.mEffectiveRowSpan, aColSpanRight, true, false,
+                  getter_AddRefs(newCellElement));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+  if (!newCellElement) {
     return NS_OK;
   }
   if (aNewCell) {
-    NS_ADDREF(*aNewCell = newCell.get());
-  }
-  return CopyCellBackgroundColor(newCell, cell);
+    NS_ADDREF(*aNewCell = newCellElement.get());
+  }
+  rv = CopyCellBackgroundColor(newCellElement, cellData.mElement);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+  return NS_OK;
 }
 
 nsresult
 HTMLEditor::SplitCellIntoRows(Element* aTable,
                               int32_t aRowIndex,
                               int32_t aColIndex,
                               int32_t aRowSpanAbove,
                               int32_t aRowSpanBelow,
@@ -2302,122 +2278,124 @@ HTMLEditor::SplitCellIntoRows(Element* a
   if (NS_WARN_IF(!aTable)) {
     return NS_ERROR_INVALID_ARG;
   }
 
   if (aNewCell) {
     *aNewCell = nullptr;
   }
 
-  RefPtr<Element> cell;
-  int32_t startRowIndex, startColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan;
-  bool    isSelected;
-  nsresult rv =
-    GetCellDataAt(aTable, aRowIndex, aColIndex, getter_AddRefs(cell),
-                  &startRowIndex, &startColIndex,
-                  &rowSpan, &colSpan,
-                  &actualRowSpan, &actualColSpan, &isSelected);
-  if (NS_WARN_IF(NS_FAILED(rv))) {
-    return rv;
-  }
-  if (NS_WARN_IF(!cell)) {
+  IgnoredErrorResult ignoredError;
+  CellData cellData(*this, *aTable, aRowIndex, aColIndex, ignoredError);
+  if (NS_WARN_IF(cellData.FailedOrNotFound())) {
     return NS_ERROR_FAILURE;
   }
 
   // We can't split!
-  if (actualRowSpan <= 1 || (aRowSpanAbove + aRowSpanBelow) > actualRowSpan) {
+  if (cellData.mEffectiveRowSpan <= 1 ||
+      aRowSpanAbove + aRowSpanBelow > cellData.mEffectiveRowSpan) {
     return NS_OK;
   }
 
   ErrorResult error;
   TableSize tableSize(*this, *aTable, error);
   if (NS_WARN_IF(error.Failed())) {
     return error.StealNSResult();
   }
 
-  RefPtr<Element> cell2;
+  // Find a cell to insert before or after
+  RefPtr<Element> cellElementAtInsertionPoint;
   RefPtr<Element> lastCellFound;
-  int32_t startRowIndex2, startColIndex2, rowSpan2, colSpan2, actualRowSpan2, actualColSpan2;
-  bool    isSelected2;
-  int32_t colIndex = 0;
-  bool insertAfter = (startColIndex > 0);
-  // This is the row we will insert new cell into
-  int32_t rowBelowIndex = startRowIndex+aRowSpanAbove;
-
-  // Find a cell to insert before or after
-  for (;;) {
-    // Search for a cell to insert before
-    rv = GetCellDataAt(aTable, rowBelowIndex,
-                       colIndex, getter_AddRefs(cell2),
-                       &startRowIndex2, &startColIndex2, &rowSpan2, &colSpan2,
-                       &actualRowSpan2, &actualColSpan2, &isSelected2);
+  bool insertAfter = (cellData.mFirst.mColumn > 0);
+  CellData cellDataAtInsertionPoint;
+  for (int32_t colIndex = 0,
+               rowBelowIndex = cellData.mFirst.mRow + aRowSpanAbove;
+       colIndex <= tableSize.mColumnCount;
+       colIndex = cellData.NextColumnIndex()) {
+    cellDataAtInsertionPoint.Update(*this, *aTable, rowBelowIndex, colIndex,
+                                    ignoredError);
     // If we fail here, it could be because row has bad rowspan values,
-    //   such as all cells having rowspan > 1 (Call FixRowSpan first!)
-    if (NS_FAILED(rv) || !cell) {
+    // such as all cells having rowspan > 1 (Call FixRowSpan first!).
+    // XXX According to the comment, this does not assume that
+    //     FixRowSpan() doesn't work well and user can create non-rectangular
+    //     table.  So, we should not return error when CellData cannot find
+    //     a cell.
+    if (NS_WARN_IF(cellDataAtInsertionPoint.FailedOrNotFound())) {
       return NS_ERROR_FAILURE;
     }
 
+    // FYI: Don't use std::move() here since the following checks will use
+    //      utility methods of cellDataAtInsertionPoint, but some of them
+    //      check whether its mElement is not nullptr.
+    cellElementAtInsertionPoint = cellDataAtInsertionPoint.mElement;
+
     // Skip over cells spanned from above (like the one we are splitting!)
-    if (cell2 && startRowIndex2 == rowBelowIndex) {
+    if (cellDataAtInsertionPoint.mElement &&
+        !cellDataAtInsertionPoint.IsSpannedFromOtherRow()) {
       if (!insertAfter) {
         // Inserting before, so stop at first cell in row we want to insert
         // into.
         break;
       }
       // New cell isn't first in row,
       // so stop after we find the cell just before new cell's column
-      if (startColIndex2 + actualColSpan2 == startColIndex) {
+      if (cellDataAtInsertionPoint.NextColumnIndex() ==
+            cellData.mFirst.mColumn) {
         break;
       }
       // If cell found is AFTER desired new cell colum,
       //  we have multiple cells with rowspan > 1 that
       //  prevented us from finding a cell to insert after...
-      if (startColIndex2 > startColIndex) {
+      if (cellDataAtInsertionPoint.mFirst.mColumn > cellData.mFirst.mColumn) {
         // ... so instead insert before the cell we found
         insertAfter = false;
         break;
       }
-      lastCellFound = cell2;
+      // FYI: Don't use std::move() here since
+      //      cellDataAtInsertionPoint.NextColumnIndex() needs it.
+      lastCellFound = cellDataAtInsertionPoint.mElement;
     }
-    // Skip to next available cellmap location
-    colIndex += std::max(actualColSpan2, 1);
-
-    // Done when past end of total number of columns
-    if (colIndex > tableSize.mColumnCount) {
-      break;
-    }
-  }
-
-  if (!cell2 && lastCellFound) {
+    MOZ_ASSERT(colIndex < cellDataAtInsertionPoint.NextColumnIndex());
+  }
+
+  if (!cellElementAtInsertionPoint && lastCellFound) {
     // Edge case where we didn't find a cell to insert after
     //  or before because column(s) before desired column
     //  and all columns after it are spanned from above.
     //  We can insert after the last cell we found
-    cell2 = lastCellFound;
+    cellElementAtInsertionPoint = std::move(lastCellFound);
     insertAfter = true; // Should always be true, but let's be sure
   }
 
   // Reduce rowspan of cell to split
-  rv = SetRowSpan(cell, aRowSpanAbove);
-  NS_ENSURE_SUCCESS(rv, rv);
-
+  nsresult rv = SetRowSpan(cellData.mElement, aRowSpanAbove);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
 
   // Insert new cell after using the remaining span
   //  and always get the new cell so we can copy the background color;
   RefPtr<Element> newCell;
-  rv = InsertCell(cell2, aRowSpanBelow, actualColSpan, insertAfter, false,
-                  getter_AddRefs(newCell));
-  NS_ENSURE_SUCCESS(rv, rv);
+  rv = InsertCell(cellElementAtInsertionPoint,
+                  aRowSpanBelow, cellData.mEffectiveColSpan,
+                  insertAfter, false, getter_AddRefs(newCell));
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
   if (!newCell) {
     return NS_OK;
   }
   if (aNewCell) {
     NS_ADDREF(*aNewCell = newCell.get());
   }
-  return CopyCellBackgroundColor(newCell, cell2);
+  rv = CopyCellBackgroundColor(newCell, cellElementAtInsertionPoint);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+  return NS_OK;
 }
 
 NS_IMETHODIMP
 HTMLEditor::SwitchTableCellHeaderType(Element* aSourceCell,
                                       Element** aNewCell)
 {
   if (NS_WARN_IF(!aSourceCell)) {
     return NS_ERROR_INVALID_ARG;
@@ -2461,31 +2439,29 @@ HTMLEditor::SwitchTableCellHeaderType(El
   return NS_OK;
 }
 
 NS_IMETHODIMP
 HTMLEditor::JoinTableCells(bool aMergeNonContiguousContents)
 {
   RefPtr<Element> table;
   RefPtr<Element> targetCell;
-  int32_t startRowIndex, startColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan;
-  bool    isSelected;
-  RefPtr<Element> cell2;
-  int32_t startRowIndex2, startColIndex2, rowSpan2, colSpan2, actualRowSpan2, actualColSpan2;
-  bool    isSelected2;
+  int32_t startRowIndex, startColIndex;
 
   // Get cell, table, etc. at selection anchor node
   nsresult rv = GetCellContext(nullptr,
                                getter_AddRefs(table),
                                getter_AddRefs(targetCell),
                                nullptr, nullptr,
                                &startRowIndex, &startColIndex);
-  NS_ENSURE_SUCCESS(rv, rv);
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
   if (!table || !targetCell) {
-    return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
+    return NS_OK;
   }
 
   AutoPlaceholderBatch beginBatching(this);
   //Don't let Rules System change the selection
   AutoTransactionsConserveSelection dontChangeSelection(*this);
 
   // Note: We dont' use AutoSelectionSetterAfterTableEdit here so the selection
   //  is retained after joining. This leaves the target cell selected
@@ -2534,164 +2510,177 @@ HTMLEditor::JoinTableCells(bool aMergeNo
 
     // This defines the last indexes along the "edges"
     //  of the contiguous block of cells, telling us
     //  that we can join adjacent cells to the block
     // Start with same as the first values,
     //  then expand as we find adjacent selected cells
     int32_t lastRowIndex = firstSelectedCell.mIndexes.mRow;
     int32_t lastColIndex = firstSelectedCell.mIndexes.mColumn;
-    int32_t rowIndex, colIndex;
-
-    // First pass: Determine boundaries of contiguous rectangular block
-    //  that we will join into one cell,
-    //  favoring adjacent cells in the same row
-    for (rowIndex = firstSelectedCell.mIndexes.mRow;
+
+    // First pass: Determine boundaries of contiguous rectangular block that
+    // we will join into one cell, favoring adjacent cells in the same row.
+    IgnoredErrorResult ignoredError;
+    for (int32_t rowIndex = firstSelectedCell.mIndexes.mRow;
          rowIndex <= lastRowIndex;
          rowIndex++) {
       int32_t currentRowCount = tableSize.mRowCount;
       // Be sure each row doesn't have rowspan errors
       rv = FixBadRowSpan(table, rowIndex, tableSize.mRowCount);
-      NS_ENSURE_SUCCESS(rv, rv);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
       // Adjust rowcount by number of rows we removed
       lastRowIndex -= currentRowCount - tableSize.mRowCount;
 
       bool cellFoundInRow = false;
       bool lastRowIsSet = false;
       int32_t lastColInRow = 0;
       int32_t firstColInRow = firstSelectedCell.mIndexes.mColumn;
-      for (colIndex = firstSelectedCell.mIndexes.mColumn;
+      int32_t colIndex = firstSelectedCell.mIndexes.mColumn;
+      for (CellData cellData;
            colIndex < tableSize.mColumnCount;
-           colIndex += std::max(actualColSpan2, 1)) {
-        rv = GetCellDataAt(table, rowIndex, colIndex, getter_AddRefs(cell2),
-                           &startRowIndex2, &startColIndex2,
-                           &rowSpan2, &colSpan2,
-                           &actualRowSpan2, &actualColSpan2, &isSelected2);
-        NS_ENSURE_SUCCESS(rv, rv);
-
-        if (isSelected2) {
+           colIndex = cellData.NextColumnIndex()) {
+        cellData.Update(*this, *table, rowIndex, colIndex, ignoredError);
+        if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+          return NS_ERROR_FAILURE;
+        }
+
+        if (cellData.mIsSelected) {
           if (!cellFoundInRow) {
             // We've just found the first selected cell in this row
-            firstColInRow = colIndex;
+            firstColInRow = cellData.mCurrent.mColumn;
           }
-          if (rowIndex > firstSelectedCell.mIndexes.mRow &&
+          if (cellData.mCurrent.mRow > firstSelectedCell.mIndexes.mRow &&
               firstColInRow != firstSelectedCell.mIndexes.mColumn) {
             // We're in at least the second row,
             // but left boundary is "ragged" (not the same as 1st row's start)
             //Let's just end block on previous row
             // and keep previous lastColIndex
             //TODO: We could try to find the Maximum firstColInRow
             //      so our block can still extend down more rows?
-            lastRowIndex = std::max(0,rowIndex - 1);
+            lastRowIndex = std::max(0, cellData.mCurrent.mRow - 1);
             lastRowIsSet = true;
             break;
           }
           // Save max selected column in this row, including extra colspan
-          lastColInRow = colIndex + (actualColSpan2-1);
+          lastColInRow = cellData.LastColumnIndex();
           cellFoundInRow = true;
         } else if (cellFoundInRow) {
           // No cell or not selected, but at least one cell in row was found
-          if (rowIndex > firstSelectedCell.mIndexes.mRow + 1 &&
-              colIndex <= lastColIndex) {
+          if (cellData.mCurrent.mRow > firstSelectedCell.mIndexes.mRow + 1 &&
+              cellData.mCurrent.mColumn <= lastColIndex) {
             // Cell is in a column less than current right border in
             //  the third or higher selected row, so stop block at the previous row
-            lastRowIndex = std::max(0,rowIndex - 1);
+            lastRowIndex = std::max(0, cellData.mCurrent.mRow - 1);
             lastRowIsSet = true;
           }
           // We're done with this row
           break;
         }
+        MOZ_ASSERT(colIndex < cellData.NextColumnIndex());
       } // End of column loop
 
       // Done with this row
       if (cellFoundInRow) {
         if (rowIndex == firstSelectedCell.mIndexes.mRow) {
           // First row always initializes the right boundary
           lastColIndex = lastColInRow;
         }
 
         // If we didn't determine last row above...
         if (!lastRowIsSet) {
           if (colIndex < lastColIndex) {
             // (don't think we ever get here?)
             // Cell is in a column less than current right boundary,
             //  so stop block at the previous row
-            lastRowIndex = std::max(0,rowIndex - 1);
+            lastRowIndex = std::max(0, rowIndex - 1);
           } else {
             // Go on to examine next row
-            lastRowIndex = rowIndex+1;
+            lastRowIndex = rowIndex + 1;
           }
         }
         // Use the minimum col we found so far for right boundary
         lastColIndex = std::min(lastColIndex, lastColInRow);
       } else {
         // No selected cells in this row -- stop at row above
         //  and leave last column at its previous value
-        lastRowIndex = std::max(0,rowIndex - 1);
+        lastRowIndex = std::max(0, rowIndex - 1);
       }
     }
 
     // The list of cells we will delete after joining
     nsTArray<RefPtr<Element>> deleteList;
 
     // 2nd pass: Do the joining and merging
-    for (rowIndex = 0; rowIndex < tableSize.mRowCount; rowIndex++) {
-      for (colIndex = 0;
+    for (int32_t rowIndex = 0; rowIndex < tableSize.mRowCount; rowIndex++) {
+      CellData cellData;
+      for (int32_t colIndex = 0;
            colIndex < tableSize.mColumnCount;
-           colIndex += std::max(actualColSpan2, 1)) {
-        rv = GetCellDataAt(table, rowIndex, colIndex, getter_AddRefs(cell2),
-                           &startRowIndex2, &startColIndex2,
-                           &rowSpan2, &colSpan2,
-                           &actualRowSpan2, &actualColSpan2, &isSelected2);
-        NS_ENSURE_SUCCESS(rv, rv);
+           colIndex = cellData.NextColumnIndex()) {
+        cellData.Update(*this, *table, rowIndex, colIndex, ignoredError);
+        if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+          return NS_ERROR_FAILURE;
+        }
 
         // If this is 0, we are past last cell in row, so exit the loop
-        if (!actualColSpan2) {
+        if (!cellData.mEffectiveColSpan) {
           break;
         }
 
         // Merge only selected cells (skip cell we're merging into, of course)
-        if (isSelected2 && cell2 != firstSelectedCell.mElement) {
-          if (rowIndex >= firstSelectedCell.mIndexes.mRow &&
-              rowIndex <= lastRowIndex &&
-              colIndex >= firstSelectedCell.mIndexes.mColumn &&
-              colIndex <= lastColIndex) {
+        if (cellData.mIsSelected &&
+            cellData.mElement != firstSelectedCell.mElement) {
+          if (cellData.mCurrent.mRow >= firstSelectedCell.mIndexes.mRow &&
+              cellData.mCurrent.mRow <= lastRowIndex &&
+              cellData.mCurrent.mColumn >= firstSelectedCell.mIndexes.mColumn &&
+              cellData.mCurrent.mColumn <= lastColIndex) {
             // We are within the join region
             // Problem: It is very tricky to delete cells as we merge,
             //  since that will upset the cellmap
             //  Instead, build a list of cells to delete and do it later
-            NS_ASSERTION(startRowIndex2 == rowIndex, "JoinTableCells: StartRowIndex is in row above");
-
-            if (actualColSpan2 > 1) {
+            NS_ASSERTION(!cellData.IsSpannedFromOtherRow(),
+                         "JoinTableCells: StartRowIndex is in row above");
+
+            if (cellData.mEffectiveColSpan > 1) {
               //Check if cell "hangs" off the boundary because of colspan > 1
               //  Use split methods to chop off excess
-              int32_t extraColSpan = (startColIndex2 + actualColSpan2) - (lastColIndex+1);
+              int32_t extraColSpan =
+                cellData.mFirst.mColumn + cellData.mEffectiveColSpan -
+                  (lastColIndex + 1);
               if ( extraColSpan > 0) {
-                rv = SplitCellIntoColumns(table, startRowIndex2, startColIndex2,
-                                          actualColSpan2 - extraColSpan,
-                                          extraColSpan, nullptr);
-                NS_ENSURE_SUCCESS(rv, rv);
+                rv = SplitCellIntoColumns(
+                       table,
+                       cellData.mFirst.mRow, cellData.mFirst.mColumn,
+                       cellData.mEffectiveColSpan - extraColSpan,
+                       extraColSpan, nullptr);
+                if (NS_WARN_IF(NS_FAILED(rv))) {
+                  return rv;
+                }
               }
             }
 
-            rv = MergeCells(firstSelectedCell.mElement, cell2, false);
+            rv =
+              MergeCells(firstSelectedCell.mElement, cellData.mElement, false);
             if (NS_WARN_IF(NS_FAILED(rv))) {
               return rv;
             }
 
             // Add cell to list to delete
-            deleteList.AppendElement(cell2.get());
+            deleteList.AppendElement(cellData.mElement.get());
           } else if (aMergeNonContiguousContents) {
             // Cell is outside join region -- just merge the contents
-            rv = MergeCells(firstSelectedCell.mElement, cell2, false);
+            rv =
+              MergeCells(firstSelectedCell.mElement, cellData.mElement, false);
             if (NS_WARN_IF(NS_FAILED(rv))) {
               return rv;
             }
           }
         }
+        MOZ_ASSERT(colIndex < cellData.NextColumnIndex());
       }
     }
 
     // All cell contents are merged. Delete the empty cells we accumulated
     // Prevent rules testing until we're done
     AutoTopLevelEditSubActionNotifier maybeTopLevelEditSubAction(
                                         *this, EditSubAction::eDeleteNode,
                                         nsIEditor::eNext);
@@ -2736,77 +2725,99 @@ HTMLEditor::JoinTableCells(bool aMergeNo
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
 
     // Fixup disturbances in table layout
     DebugOnly<nsresult> rv = NormalizeTable(*selection, *table);
     NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to normalize the table");
   } else {
-    // Joining with cell to the right -- get rowspan and colspan data of target cell
-    rv = GetCellDataAt(table, startRowIndex, startColIndex,
-                       getter_AddRefs(targetCell),
-                       &startRowIndex, &startColIndex, &rowSpan, &colSpan,
-                       &actualRowSpan, &actualColSpan, &isSelected);
-    NS_ENSURE_SUCCESS(rv, rv);
-    NS_ENSURE_TRUE(targetCell, NS_ERROR_NULL_POINTER);
-
-    // Get data for cell to the right
-    rv = GetCellDataAt(table, startRowIndex, startColIndex + actualColSpan,
-                       getter_AddRefs(cell2),
-                       &startRowIndex2, &startColIndex2, &rowSpan2, &colSpan2,
-                       &actualRowSpan2, &actualColSpan2, &isSelected2);
-    NS_ENSURE_SUCCESS(rv, rv);
-    if (!cell2) {
+    // Joining with cell to the right -- get rowspan and colspan data of target
+    // cell.
+    IgnoredErrorResult ignoredError;
+    CellData leftCellData(*this, *table, startRowIndex, startColIndex,
+                          ignoredError);
+    if (NS_WARN_IF(leftCellData.FailedOrNotFound())) {
+      return NS_ERROR_FAILURE;
+    }
+
+    // Get data for cell to the right.
+    CellData rightCellData(
+               *this, *table,
+               leftCellData.mFirst.mRow,
+               leftCellData.mFirst.mColumn + leftCellData.mEffectiveColSpan,
+               ignoredError);
+    if (NS_WARN_IF(rightCellData.FailedOrNotFound())) {
+      return NS_ERROR_FAILURE;
+    }
+
+    // XXX So, this does not assume that CellData returns error when just not
+    //     found.  We need to fix this later.
+    if (!rightCellData.mElement) {
       return NS_OK; // Don't fail if there's no cell
     }
 
     // sanity check
-    NS_ASSERTION((startRowIndex >= startRowIndex2),"JoinCells: startRowIndex < startRowIndex2");
+    NS_ASSERTION(rightCellData.mCurrent.mRow >= rightCellData.mFirst.mRow,
+      "JoinCells: rightCellData.mCurrent.mRow < rightCellData.mFirst.mRow");
 
     // Figure out span of merged cell starting from target's starting row
     // to handle case of merged cell starting in a row above
-    int32_t spanAboveMergedCell = startRowIndex - startRowIndex2;
-    int32_t effectiveRowSpan2 = actualRowSpan2 - spanAboveMergedCell;
-
-    if (effectiveRowSpan2 > actualRowSpan) {
+    int32_t spanAboveMergedCell = rightCellData.NumberOfPrecedingRows();
+    int32_t effectiveRowSpan2 =
+      rightCellData.mEffectiveRowSpan - spanAboveMergedCell;
+    if (effectiveRowSpan2 > leftCellData.mEffectiveRowSpan) {
       // Cell to the right spans into row below target
       // Split off portion below target cell's bottom-most row
-      rv = SplitCellIntoRows(table, startRowIndex2, startColIndex2,
-                             spanAboveMergedCell+actualRowSpan,
-                             effectiveRowSpan2-actualRowSpan, nullptr);
-      NS_ENSURE_SUCCESS(rv, rv);
+      rv =
+        SplitCellIntoRows(table,
+                          rightCellData.mFirst.mRow,
+                          rightCellData.mFirst.mColumn,
+                          spanAboveMergedCell + leftCellData.mEffectiveRowSpan,
+                          effectiveRowSpan2 - leftCellData.mEffectiveRowSpan,
+                          nullptr);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
     }
 
     // Move contents from cell to the right
     // Delete the cell now only if it starts in the same row
     //   and has enough row "height"
-    rv = MergeCells(targetCell, cell2,
-                    (startRowIndex2 == startRowIndex) &&
-                    (effectiveRowSpan2 >= actualRowSpan));
-    NS_ENSURE_SUCCESS(rv, rv);
-
-    if (effectiveRowSpan2 < actualRowSpan) {
+    rv = MergeCells(leftCellData.mElement, rightCellData.mElement,
+                    !rightCellData.IsSpannedFromOtherRow() &&
+                    effectiveRowSpan2 >= leftCellData.mEffectiveRowSpan);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    if (effectiveRowSpan2 < leftCellData.mEffectiveRowSpan) {
       // Merged cell is "shorter"
       // (there are cells(s) below it that are row-spanned by target cell)
       // We could try splitting those cells, but that's REAL messy,
       //  so the safest thing to do is NOT really join the cells
       return NS_OK;
     }
 
     if (spanAboveMergedCell > 0) {
       // Cell we merged started in a row above the target cell
       // Reduce rowspan to give room where target cell will extend its colspan
-      rv = SetRowSpan(cell2, spanAboveMergedCell);
-      NS_ENSURE_SUCCESS(rv, rv);
+      rv = SetRowSpan(rightCellData.mElement, spanAboveMergedCell);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
     }
 
     // Reset target cell's colspan to encompass cell to the right
-    rv = SetColSpan(targetCell, actualColSpan+actualColSpan2);
-    NS_ENSURE_SUCCESS(rv, rv);
+    rv = SetColSpan(leftCellData.mElement,
+                    leftCellData.mEffectiveColSpan +
+                      rightCellData.mEffectiveColSpan);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
   }
   return NS_OK;
 }
 
 nsresult
 HTMLEditor::MergeCells(RefPtr<Element> aTargetCell,
                        RefPtr<Element> aCellToMerge,
                        bool aDeleteCellToMerge)
@@ -2884,72 +2895,71 @@ HTMLEditor::FixBadRowSpan(Element* aTabl
   }
 
   ErrorResult error;
   TableSize tableSize(*this, *aTable, error);
   if (NS_WARN_IF(error.Failed())) {
     return error.StealNSResult();
   }
 
-  RefPtr<Element> cell;
-  int32_t startRowIndex, startColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan;
-  bool    isSelected;
-
   int32_t minRowSpan = -1;
-  int32_t colIndex;
-
-  for (colIndex = 0;
+  IgnoredErrorResult ignoredError;
+  CellData cellData;
+  for (int32_t colIndex = 0;
        colIndex < tableSize.mColumnCount;
-       colIndex += std::max(actualColSpan, 1)) {
-    nsresult rv =
-      GetCellDataAt(aTable, aRowIndex, colIndex, getter_AddRefs(cell),
-                    &startRowIndex, &startColIndex, &rowSpan, &colSpan,
-                    &actualRowSpan, &actualColSpan, &isSelected);
+       colIndex = cellData.NextColumnIndex()) {
+    cellData.Update(*this, *aTable, aRowIndex, colIndex, ignoredError);
     // NOTE: This is a *real* failure.
-    // GetCellDataAt passes if cell is missing from cellmap
+    // CellData passes if cell is missing from cellmap
     // XXX If <table> has large rowspan value or colspan value than actual
     //     cells, we may hit error.  So, this method is always failed to
     //     "fix" the rowspan...
-    if (NS_WARN_IF(NS_FAILED(rv))) {
-      return rv;
+    if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+      return NS_ERROR_FAILURE;
     }
-    if (!cell) {
+
+    // XXX So, this does not assume that CellData returns error when just not
+    //     found.  We need to fix this later.
+    if (!cellData.mElement) {
       break;
     }
-    if (rowSpan > 0 &&
-        startRowIndex == aRowIndex &&
-        (rowSpan < minRowSpan || minRowSpan == -1)) {
-      minRowSpan = rowSpan;
+
+    if (cellData.mRowSpan > 0 &&
+        !cellData.IsSpannedFromOtherRow() &&
+        (cellData.mRowSpan < minRowSpan || minRowSpan == -1)) {
+      minRowSpan = cellData.mRowSpan;
     }
-    NS_ASSERTION((actualColSpan > 0),"ActualColSpan = 0 in FixBadRowSpan");
-  }
+    MOZ_ASSERT(colIndex < cellData.NextColumnIndex());
+  }
+
   if (minRowSpan > 1) {
     // The amount to reduce everyone's rowspan
     // so at least one cell has rowspan = 1
     int32_t rowsReduced = minRowSpan - 1;
-    for (colIndex = 0;
+    CellData cellData;
+    for (int32_t colIndex = 0;
          colIndex < tableSize.mColumnCount;
-         colIndex += std::max(actualColSpan, 1)) {
-      nsresult rv =
-        GetCellDataAt(aTable, aRowIndex, colIndex, getter_AddRefs(cell),
-                      &startRowIndex, &startColIndex, &rowSpan, &colSpan,
-                      &actualRowSpan, &actualColSpan, &isSelected);
-      if (NS_WARN_IF(NS_FAILED(rv))) {
-        return rv;
+         colIndex = cellData.NextColumnIndex()) {
+      cellData.Update(*this, *aTable, aRowIndex, colIndex, ignoredError);
+      if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+        return NS_ERROR_FAILURE;
       }
+
       // Fixup rowspans only for cells starting in current row
-      if (cell && rowSpan > 0 &&
-          startRowIndex == aRowIndex &&
-          startColIndex ==  colIndex ) {
-        rv = SetRowSpan(cell, rowSpan-rowsReduced);
+      // XXX So, this does not assume that CellData returns error when just
+      //     not found a cell.  Fix this later.
+      if (cellData.mElement && cellData.mRowSpan > 0 &&
+          !cellData.IsSpannedFromOtherRowOrColumn()) {
+        nsresult rv =
+          SetRowSpan(cellData.mElement, cellData.mRowSpan - rowsReduced);
         if (NS_WARN_IF(NS_FAILED(rv))) {
           return rv;
         }
       }
-      NS_ASSERTION((actualColSpan > 0),"ActualColSpan = 0 in FixBadRowSpan");
+      MOZ_ASSERT(colIndex < cellData.NextColumnIndex());
     }
   }
   tableSize.Update(*this, *aTable, error);
   if (NS_WARN_IF(error.Failed())) {
     return error.StealNSResult();
   }
   aNewRowCount = tableSize.mRowCount;
   return NS_OK;
@@ -2965,72 +2975,70 @@ HTMLEditor::FixBadColSpan(Element* aTabl
   }
 
   ErrorResult error;
   TableSize tableSize(*this, *aTable, error);
   if (NS_WARN_IF(error.Failed())) {
     return error.StealNSResult();
   }
 
-  RefPtr<Element> cell;
-  int32_t startRowIndex, startColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan;
-  bool    isSelected;
-
   int32_t minColSpan = -1;
-  int32_t rowIndex;
-
-  for (rowIndex = 0;
+  IgnoredErrorResult ignoredError;
+  CellData cellData;
+  for (int32_t rowIndex = 0;
        rowIndex < tableSize.mRowCount;
-       rowIndex += std::max(actualRowSpan, 1)) {
-    nsresult rv =
-      GetCellDataAt(aTable, rowIndex, aColIndex, getter_AddRefs(cell),
-                    &startRowIndex, &startColIndex, &rowSpan, &colSpan,
-                    &actualRowSpan, &actualColSpan, &isSelected);
+       rowIndex = cellData.NextRowIndex()) {
+    cellData.Update(*this, *aTable, rowIndex, aColIndex, ignoredError);
     // NOTE: This is a *real* failure.
-    // GetCellDataAt passes if cell is missing from cellmap
+    // CellData passes if cell is missing from cellmap
     // XXX If <table> has large rowspan value or colspan value than actual
     //     cells, we may hit error.  So, this method is always failed to
     //     "fix" the colspan...
-    if (NS_WARN_IF(NS_FAILED(rv))) {
-      return rv;
+    if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+      return NS_ERROR_FAILURE;
     }
-    if (!cell) {
+
+    // XXX So, this does not assume that CellData returns error when just
+    //     not found a cell.  Fix this later.
+    if (!cellData.mElement) {
       break;
     }
-    if (colSpan > 0 &&
-        startColIndex == aColIndex &&
-        (colSpan < minColSpan || minColSpan == -1)) {
-      minColSpan = colSpan;
+    if (cellData.mColSpan > 0 &&
+        !cellData.IsSpannedFromOtherColumn() &&
+        (cellData.mColSpan < minColSpan || minColSpan == -1)) {
+      minColSpan = cellData.mColSpan;
     }
-    NS_ASSERTION((actualRowSpan > 0),"ActualRowSpan = 0 in FixBadColSpan");
-  }
+    MOZ_ASSERT(rowIndex < cellData.NextRowIndex());
+  }
+
   if (minColSpan > 1) {
     // The amount to reduce everyone's colspan
     // so at least one cell has colspan = 1
     int32_t colsReduced = minColSpan - 1;
-    for (rowIndex = 0;
+    CellData cellData;
+    for (int32_t rowIndex = 0;
          rowIndex < tableSize.mRowCount;
-         rowIndex += std::max(actualRowSpan, 1)) {
-      nsresult rv =
-        GetCellDataAt(aTable, rowIndex, aColIndex, getter_AddRefs(cell),
-                      &startRowIndex, &startColIndex, &rowSpan, &colSpan,
-                      &actualRowSpan, &actualColSpan, &isSelected);
-      if (NS_WARN_IF(NS_FAILED(rv))) {
-        return rv;
+         rowIndex = cellData.NextRowIndex()) {
+      cellData.Update(*this, *aTable, rowIndex, aColIndex, ignoredError);
+      if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+        return NS_ERROR_FAILURE;
       }
+
       // Fixup colspans only for cells starting in current column
-      if (cell && colSpan > 0 &&
-          startColIndex == aColIndex &&
-          startRowIndex ==  rowIndex) {
-        rv = SetColSpan(cell, colSpan-colsReduced);
+      // XXX So, this does not assume that CellData returns error when just
+      //     not found a cell.  Fix this later.
+      if (cellData.mElement && cellData.mColSpan > 0 &&
+          !cellData.IsSpannedFromOtherRowOrColumn()) {
+        nsresult rv =
+          SetColSpan(cellData.mElement, cellData.mColSpan - colsReduced);
         if (NS_WARN_IF(NS_FAILED(rv))) {
           return rv;
         }
       }
-      NS_ASSERTION((actualRowSpan > 0),"ActualRowSpan = 0 in FixBadColSpan");
+      MOZ_ASSERT(rowIndex < cellData.NextRowIndex());
     }
   }
   tableSize.Update(*this, *aTable, error);
   if (NS_WARN_IF(error.Failed())) {
     return error.StealNSResult();
   }
   aNewColCount = tableSize.mColumnCount;
   return NS_OK;
@@ -3103,58 +3111,55 @@ HTMLEditor::NormalizeTable(Selection& aS
   for (int32_t colIndex = 0; colIndex < tableSize.mColumnCount; colIndex++) {
     nsresult rv = FixBadColSpan(tableElement, colIndex, tableSize.mColumnCount);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
   }
 
   // Fill in missing cellmap locations with empty cells
+  IgnoredErrorResult ignoredError;
   for (int32_t rowIndex = 0; rowIndex < tableSize.mRowCount; rowIndex++) {
     RefPtr<Element> previousCellElementInRow;
     for (int32_t colIndex = 0; colIndex < tableSize.mColumnCount; colIndex++) {
-      int32_t startRowIndex = 0, startColIndex = 0;
-      int32_t rowSpan = 0, colSpan = 0;
-      int32_t actualRowSpan = 0, actualColSpan = 0;
-      bool isSelected;
-      RefPtr<Element> cellElement;
+      CellData cellData(*this, *tableElement, rowIndex, colIndex, ignoredError);
+      // NOTE: This is a *real* failure.
+      // CellData passes if cell is missing from cellmap
+      // XXX So, this method assumes that CellData won't return error when
+      //     just not found.  Fix this later.
+      if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+        return NS_ERROR_FAILURE;
+      }
+
+      if (cellData.mElement) {
+        // Save the last cell found in the same row we are scanning
+        if (!cellData.IsSpannedFromOtherRow()) {
+          previousCellElementInRow = std::move(cellData.mElement);
+        }
+        continue;
+      }
+
+      // We are missing a cell at a cellmap location.
+      // Add a cell after the previous cell element in the current row.
+      if (NS_WARN_IF(!previousCellElementInRow)) {
+        // We don't have any cells in this row -- We are really messed up!
+        return NS_ERROR_FAILURE;
+      }
+
+      // Insert a new cell after (true), and return the new cell to us
+      RefPtr<Element> newCellElement;
       nsresult rv =
-        GetCellDataAt(tableElement, rowIndex, colIndex,
-                      getter_AddRefs(cellElement),
-                      &startRowIndex, &startColIndex, &rowSpan, &colSpan,
-                      &actualRowSpan, &actualColSpan, &isSelected);
-      // NOTE: This is a *real* failure.
-      // GetCellDataAt passes if cell is missing from cellmap
+        InsertCell(previousCellElementInRow, 1, 1, true, false,
+                   getter_AddRefs(newCellElement));
       if (NS_WARN_IF(NS_FAILED(rv))) {
         return rv;
       }
-      if (!cellElement) {
-        // We are missing a cell at a cellmap location.
-        // Add a cell after the previous cell element in the current row.
-        if (NS_WARN_IF(!previousCellElementInRow)) {
-          // We don't have any cells in this row -- We are really messed up!
-          return NS_ERROR_FAILURE;
-        }
-
-        // Insert a new cell after (true), and return the new cell to us
-        rv = InsertCell(previousCellElementInRow, 1, 1, true, false,
-                        getter_AddRefs(cellElement));
-        if (NS_WARN_IF(NS_FAILED(rv))) {
-          return rv;
-        }
-
-        // Set this so we use returned new "cell" to set
-        // previousCellElementInRow below.
-        if (cellElement) {
-          startRowIndex = rowIndex;
-        }
-      }
-      // Save the last cell found in the same row we are scanning
-      if (startRowIndex == rowIndex) {
-        previousCellElementInRow = cellElement;
+
+      if (newCellElement) {
+        previousCellElementInRow = std::move(newCellElement);
       }
     }
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 HTMLEditor::GetCellIndexes(Element* aCellElement,
@@ -3255,46 +3260,34 @@ HTMLEditor::GetNumberOfCellsInRow(Elemen
 {
   IgnoredErrorResult ignoredError;
   TableSize tableSize(*this, aTableElement, ignoredError);
   if (NS_WARN_IF(ignoredError.Failed())) {
     return -1;
   }
 
   int32_t numberOfCells = 0;
-  for (int32_t columnIndex = 0; columnIndex < tableSize.mColumnCount;) {
-    RefPtr<Element> cellElement;
-    int32_t startRowIndex = 0, startColIndex = 0;
-    int32_t rowSpan = 0, colSpan = 0;
-    int32_t actualRowSpan = 0, actualColSpan = 0;
-    bool isSelected = false;
-    nsresult rv =
-      GetCellDataAt(&aTableElement, aRowIndex, columnIndex,
-                    getter_AddRefs(cellElement),
-                    &startRowIndex, &startColIndex, &rowSpan, &colSpan,
-                    &actualRowSpan, &actualColSpan, &isSelected);
+  CellData cellData;
+  for (int32_t columnIndex = 0;
+       columnIndex < tableSize.mColumnCount;
+       columnIndex = cellData.NextColumnIndex()) {
+    cellData.Update(*this, aTableElement, aRowIndex, columnIndex, ignoredError);
     // Failure means that there is no more cell in the row.  In this case,
     // we shouldn't return error since we just reach the end of the row.
-    // XXX Ideally, GetCellDataAt() should return
-    //     NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND in such case instead of
-    //     error.  However, it's used by a lot of methods, so, it's really
-    //     risky to change it.
-    if (NS_FAILED(rv)) {
+    // XXX So, this method assumes that CellData won't return error when
+    //     just not found.  Fix this later.
+    if (cellData.FailedOrNotFound()) {
       break;
     }
-    if (cellElement) {
-      // Only count cells that start in row we are working with
-      if (startRowIndex == aRowIndex) {
-        numberOfCells++;
-      }
-      // Next possible location for a cell
-      columnIndex += actualColSpan;
-    } else {
-      columnIndex++;
+
+    // Only count cells that start in row we are working with
+    if (cellData.mElement && !cellData.IsSpannedFromOtherRow()) {
+      numberOfCells++;
     }
+    MOZ_ASSERT(columnIndex < cellData.NextColumnIndex());
   }
   return numberOfCells;
 }
 
 NS_IMETHODIMP
 HTMLEditor::GetTableSize(Element* aTableOrElementInTable,
                          int32_t* aRowCount,
                          int32_t* aColumnCount)
@@ -3353,88 +3346,125 @@ HTMLEditor::TableSize::Update(HTMLEditor
     aRv.Throw(NS_ERROR_FAILURE);
     return;
   }
   mRowCount = tableFrame->GetRowCount();
   mColumnCount = tableFrame->GetColCount();
 }
 
 NS_IMETHODIMP
-HTMLEditor::GetCellDataAt(Element* aTable,
+HTMLEditor::GetCellDataAt(Element* aTableElement,
                           int32_t aRowIndex,
-                          int32_t aColIndex,
-                          Element** aCell,
+                          int32_t aColumnIndex,
+                          Element** aCellElement,
                           int32_t* aStartRowIndex,
-                          int32_t* aStartColIndex,
+                          int32_t* aStartColumnIndex,
                           int32_t* aRowSpan,
                           int32_t* aColSpan,
-                          int32_t* aActualRowSpan,
-                          int32_t* aActualColSpan,
+                          int32_t* aEffectiveRowSpan,
+                          int32_t* aEffectiveColSpan,
                           bool* aIsSelected)
 {
-  NS_ENSURE_ARG_POINTER(aStartRowIndex);
-  NS_ENSURE_ARG_POINTER(aStartColIndex);
-  NS_ENSURE_ARG_POINTER(aRowSpan);
-  NS_ENSURE_ARG_POINTER(aColSpan);
-  NS_ENSURE_ARG_POINTER(aActualRowSpan);
-  NS_ENSURE_ARG_POINTER(aActualColSpan);
-  NS_ENSURE_ARG_POINTER(aIsSelected);
-  NS_ENSURE_TRUE(aCell, NS_ERROR_NULL_POINTER);
+  if (NS_WARN_IF(!aCellElement) ||
+      NS_WARN_IF(!aStartRowIndex) || NS_WARN_IF(!aStartColumnIndex) ||
+      NS_WARN_IF(!aRowSpan) || NS_WARN_IF(!aColSpan) ||
+      NS_WARN_IF(!aEffectiveRowSpan) || NS_WARN_IF(!aEffectiveColSpan) ||
+      NS_WARN_IF(!aIsSelected)) {
+    return NS_ERROR_INVALID_ARG;
+  }
 
   *aStartRowIndex = 0;
-  *aStartColIndex = 0;
+  *aStartColumnIndex = 0;
   *aRowSpan = 0;
   *aColSpan = 0;
-  *aActualRowSpan = 0;
-  *aActualColSpan = 0;
+  *aEffectiveRowSpan = 0;
+  *aEffectiveColSpan = 0;
   *aIsSelected = false;
-
-  *aCell = nullptr;
-
-  // needs to live while we use aTable
-  // XXX Really? Looks like it's safe to use raw pointer here.
-  //     However, layout code change won't be handled by editor developers
-  //     so that it must be safe to keep using RefPtr here.
-  RefPtr<Element> table;
-  if (!aTable) {
+  *aCellElement = nullptr;
+
+  // Let's keep the table element with strong pointer since editor developers
+  // may not handle layout code of <table>, however, this method depends on
+  // them.
+  RefPtr<Element> table = aTableElement;
+  if (!table) {
     RefPtr<Selection> selection = GetSelection();
     if (NS_WARN_IF(!selection)) {
       return NS_ERROR_FAILURE;
     }
     // Get the selected table or the table enclosing the selection anchor.
     table =
       GetElementOrParentByTagNameAtSelection(*selection, *nsGkAtoms::table);
     if (NS_WARN_IF(!table)) {
       return NS_ERROR_FAILURE;
     }
-    aTable = table;
-  }
-
-  nsTableWrapperFrame* tableFrame = HTMLEditor::GetTableFrame(aTable);
-  NS_ENSURE_TRUE(tableFrame, NS_ERROR_FAILURE);
-
-  nsTableCellFrame* cellFrame =
-    tableFrame->GetCellFrameAt(aRowIndex, aColIndex);
-  if (NS_WARN_IF(!cellFrame)) {
+  }
+
+  IgnoredErrorResult ignoredError;
+  CellData cellData(*this, *table, aRowIndex, aColumnIndex, ignoredError);
+  if (NS_WARN_IF(cellData.FailedOrNotFound())) {
     return NS_ERROR_FAILURE;
   }
-
-  *aIsSelected = cellFrame->IsSelected();
-  *aStartRowIndex = cellFrame->RowIndex();
-  *aStartColIndex = cellFrame->ColIndex();
-  *aRowSpan = cellFrame->GetRowSpan();
-  *aColSpan = cellFrame->GetColSpan();
-  *aActualRowSpan = tableFrame->GetEffectiveRowSpanAt(aRowIndex, aColIndex);
-  *aActualColSpan = tableFrame->GetEffectiveColSpanAt(aRowIndex, aColIndex);
-  RefPtr<Element> domCell = cellFrame->GetContent()->AsElement();
-  domCell.forget(aCell);
-
+  cellData.mElement.forget(aCellElement);
+  *aIsSelected = cellData.mIsSelected;
+  *aStartRowIndex = cellData.mFirst.mRow;
+  *aStartColumnIndex = cellData.mFirst.mColumn;
+  *aRowSpan = cellData.mRowSpan;
+  *aColSpan = cellData.mColSpan;
+  *aEffectiveRowSpan = cellData.mEffectiveRowSpan;
+  *aEffectiveColSpan = cellData.mEffectiveColSpan;
   return NS_OK;
 }
 
+void
+HTMLEditor::CellData::Update(HTMLEditor& aHTMLEditor,
+                             Element& aTableElement,
+                             ErrorResult& aRv)
+{
+  MOZ_ASSERT(!aRv.Failed());
+
+  mElement = nullptr;
+  mIsSelected = false;
+  mFirst.mRow = -1;
+  mFirst.mColumn = -1;
+  mRowSpan = -1;
+  mColSpan = -1;
+  mEffectiveRowSpan = -1;
+  mEffectiveColSpan = -1;
+
+  nsTableWrapperFrame* tableFrame = HTMLEditor::GetTableFrame(&aTableElement);
+  if (NS_WARN_IF(!tableFrame)) {
+    aRv.Throw(NS_ERROR_FAILURE);
+    return;
+  }
+
+  // If there is no cell at the indexes.  Don't return error.
+  // XXX If we have pending layout and that causes the cell frame hasn't been
+  //     created, we should return error, but how can we do it?
+  nsTableCellFrame* cellFrame =
+    tableFrame->GetCellFrameAt(mCurrent.mRow, mCurrent.mColumn);
+  if (!cellFrame) {
+    return;
+  }
+
+  mElement = cellFrame->GetContent()->AsElement();
+  if (NS_WARN_IF(!mElement)) {
+    aRv.Throw(NS_ERROR_FAILURE);
+    return;
+  }
+  mIsSelected = cellFrame->IsSelected();
+  mFirst.mRow = cellFrame->RowIndex();
+  mFirst.mColumn = cellFrame->ColIndex();
+  mRowSpan = cellFrame->GetRowSpan();
+  mColSpan = cellFrame->GetColSpan();
+  mEffectiveRowSpan =
+    tableFrame->GetEffectiveRowSpanAt(mCurrent.mRow, mCurrent.mColumn);
+  mEffectiveColSpan =
+    tableFrame->GetEffectiveColSpanAt(mCurrent.mRow, mCurrent.mColumn);
+}
+
 NS_IMETHODIMP
 HTMLEditor::GetCellAt(Element* aTableElement,
                       int32_t aRowIndex,
                       int32_t aColumnIndex,
                       Element** aCellElement)
 {
   if (NS_WARN_IF(!aCellElement)) {
     return NS_ERROR_INVALID_ARG;
@@ -3652,17 +3682,17 @@ HTMLEditor::GetCellFromRange(nsRange* aR
   }
 
   // If a cell is deleted, the range is collapse
   //   (startOffset == aRange->EndOffset())
   //   so tell caller the cell wasn't found
   if (startContainer == endContainer &&
       aRange->EndOffset() == startOffset+1 &&
       HTMLEditUtils::IsTableCell(childNode)) {
-    // Should we also test if frame is selected? (Use GetCellDataAt())
+    // Should we also test if frame is selected? (Use CellData)
     // (Let's not for now -- more efficient)
     RefPtr<Element> cellElement = childNode->AsElement();
     cellElement.forget(aCell);
     return NS_OK;
   }
   return NS_SUCCESS_EDITOR_ELEMENT_NOT_FOUND;
 }
 
@@ -4204,67 +4234,85 @@ HTMLEditor::GetSelectedCellsType(Element
   return NS_OK;
 }
 
 bool
 HTMLEditor::AllCellsInRowSelected(Element* aTable,
                                   int32_t aRowIndex,
                                   int32_t aNumberOfColumns)
 {
-  NS_ENSURE_TRUE(aTable, false);
-
-  int32_t curStartRowIndex, curStartColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan;
-  bool    isSelected;
-
-  for (int32_t col = 0; col < aNumberOfColumns;
-       col += std::max(actualColSpan, 1)) {
-    RefPtr<Element> cell;
-    nsresult rv = GetCellDataAt(aTable, aRowIndex, col, getter_AddRefs(cell),
-                                &curStartRowIndex, &curStartColIndex,
-                                &rowSpan, &colSpan,
-                                &actualRowSpan, &actualColSpan, &isSelected);
-
-    NS_ENSURE_SUCCESS(rv, false);
-    // If no cell, we may have a "ragged" right edge,
-    //   so return TRUE only if we already found a cell in the row
-    NS_ENSURE_TRUE(cell, (col > 0) ? true : false);
-
-    // Return as soon as a non-selected cell is found
-    NS_ENSURE_TRUE(isSelected, false);
-
-    NS_ASSERTION((actualColSpan > 0),"ActualColSpan = 0 in AllCellsInRowSelected");
+  if (NS_WARN_IF(!aTable)) {
+    return false;
+  }
+
+  IgnoredErrorResult ignoredError;
+  CellData cellData;
+  for (int32_t col = 0;
+       col < aNumberOfColumns;
+       col = cellData.NextColumnIndex()) {
+    cellData.Update(*this, *aTable, aRowIndex, col, ignoredError);
+    if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+      return false;
+    }
+
+    // If no cell, we may have a "ragged" right edge, so return TRUE only if
+    // we already found a cell in the row.
+    // XXX So, this does not assume that CellData returns error when just
+    //     not found a cell.  Fix this later.
+    if (NS_WARN_IF(!cellData.mElement)) {
+      return cellData.mCurrent.mColumn > 0;
+    }
+
+    // Return as soon as a non-selected cell is found.
+    // XXX Odd, this is testing if each cell element is selected.  Why do
+    //     we need to warn if it's false??
+    if (NS_WARN_IF(!cellData.mIsSelected)) {
+      return false;
+    }
+
+    MOZ_ASSERT(col < cellData.NextColumnIndex());
   }
   return true;
 }
 
 bool
 HTMLEditor::AllCellsInColumnSelected(Element* aTable,
                                      int32_t aColIndex,
                                      int32_t aNumberOfRows)
 {
-  NS_ENSURE_TRUE(aTable, false);
-
-  int32_t curStartRowIndex, curStartColIndex, rowSpan, colSpan, actualRowSpan, actualColSpan;
-  bool    isSelected;
-
-  for (int32_t row = 0; row < aNumberOfRows;
-       row += std::max(actualRowSpan, 1)) {
-    RefPtr<Element> cell;
-    nsresult rv = GetCellDataAt(aTable, row, aColIndex, getter_AddRefs(cell),
-                                &curStartRowIndex, &curStartColIndex,
-                                &rowSpan, &colSpan,
-                                &actualRowSpan, &actualColSpan, &isSelected);
-
-    NS_ENSURE_SUCCESS(rv, false);
-    // If no cell, we must have a "ragged" right edge on the last column
-    //   so return TRUE only if we already found a cell in the row
-    NS_ENSURE_TRUE(cell, (row > 0) ? true : false);
-
-    // Return as soon as a non-selected cell is found
-    NS_ENSURE_TRUE(isSelected, false);
+  if (NS_WARN_IF(!aTable)) {
+    return false;
+  }
+
+  IgnoredErrorResult ignoredError;
+  CellData cellData;
+  for (int32_t row = 0;
+       row < aNumberOfRows;
+       row = cellData.NextRowIndex()) {
+    cellData.Update(*this, *aTable, row, aColIndex, ignoredError);
+    if (NS_WARN_IF(cellData.FailedOrNotFound())) {
+      return false;
+    }
+
+    // If no cell, we must have a "ragged" right edge on the last column so
+    // return TRUE only if we already found a cell in the row.
+    // XXX So, this does not assume that CellData returns error when just
+    //     not found a cell.  Fix this later.
+    if (NS_WARN_IF(!cellData.mElement)) {
+      return cellData.mCurrent.mRow > 0;
+    }
+
+    // Return as soon as a non-selected cell is found.
+    // XXX Odd, this is testing if each cell element is selected.  Why do
+    //     we need to warn if it's false??
+    if (NS_WARN_IF(!cellData.mIsSelected)) {
+      return false;
+    }
+
+    MOZ_ASSERT(row < cellData.NextRowIndex());
   }
   return true;
 }
 
 bool
 HTMLEditor::IsEmptyCell(dom::Element* aCell)
 {
   MOZ_ASSERT(aCell);
--- a/editor/libeditor/tests/mochitest.ini
+++ b/editor/libeditor/tests/mochitest.ini
@@ -286,16 +286,17 @@ skip-if = android_version == '24'
 [test_nsIHTMLEditor_selectElement.html]
 [test_nsIHTMLEditor_setCaretAfterElement.html]
 [test_nsIHTMLObjectResizer_hideResizers.html]
 [test_nsITableEditor_deleteTableCell.html]
 [test_nsITableEditor_deleteTableCellContents.html]
 [test_nsITableEditor_deleteTableColumn.html]
 [test_nsITableEditor_deleteTableRow.html]
 [test_nsITableEditor_getCellAt.html]
+[test_nsITableEditor_getCellDataAt.html]
 [test_nsITableEditor_getCellIndexes.html]
 [test_nsITableEditor_getFirstRow.html]
 [test_nsITableEditor_getFirstSelectedCell.html]
 [test_nsITableEditor_getFirstSelectedCellInTable.html]
 [test_nsITableEditor_getNextSelectedCell.html]
 [test_nsITableEditor_getSelectedOrParentTableElement.html]
 [test_nsITableEditor_getTableSize.html]
 [test_nsITableEditor_insertTableCell.html]
new file mode 100644
--- /dev/null
+++ b/editor/libeditor/tests/test_nsITableEditor_getCellDataAt.html
@@ -0,0 +1,756 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Test for nsITableEditor.getCellDataAt()</title>
+  <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="display">
+</div>
+<div id="content" contenteditable>out of table<table><tr><td>default content</td></tr></table></div>
+<pre id="test">
+</pre>
+
+<script class="testbody" type="application/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+  let editor = document.getElementById("content");
+  let selection = document.getSelection();
+
+  let cellElementWrapper;
+  let startRowIndexWrapper, startColumnIndexWrapper;
+  let rowspanWrapper, colspanWrapper;
+  let effectiveRowspanWrapper, effectiveColspanWrapper;
+  let isSelectedWrapper;
+
+  function reset() {
+    cellElementWrapper = {};
+    startRowIndexWrapper = {};
+    startColumnIndexWrapper = {};
+    rowspanWrapper = {};
+    colspanWrapper = {};
+    effectiveRowspanWrapper = {};
+    effectiveColspanWrapper = {};
+    isSelectedWrapper = {};
+  }
+
+  editor.focus();
+  selection.collapse(editor.firstChild, 0);
+  try {
+    getTableEditor().getCellDataAt(null, 0, 0,
+                                   cellElementWrapper,
+                                   startRowIndexWrapper, startColumnIndexWrapper,
+                                   rowspanWrapper, colspanWrapper,
+                                   effectiveRowspanWrapper, effectiveColspanWrapper,
+                                   isSelectedWrapper);
+    ok(false, "getTableEditor().getCellDataAt(null, 0, 0) should throw exception when selection is outside of any <table>s");
+  } catch (e) {
+    ok(true, "getTableEditor().getCellDataAt(null, 0, 0) should throw exception when selection is outside of any <table>s");
+  }
+
+  selection.removeAllRanges();
+  try {
+    getTableEditor().getCellDataAt(null, 0, 0,
+                                   cellElementWrapper,
+                                   startRowIndexWrapper, startColumnIndexWrapper,
+                                   rowspanWrapper, colspanWrapper,
+                                   effectiveRowspanWrapper, effectiveColspanWrapper,
+                                   isSelectedWrapper);
+    ok(false, "getTableEditor().getCellDataAt(null, 0, 0) should throw exception when selection has no ranges");
+  } catch (e) {
+    ok(true, "getTableEditor().getCellDataAt(null, 0, 0) should throw exception when selection has no ranges");
+  }
+
+  // Collapse in text node in the cell element.
+  selection.collapse(editor.firstChild.nextSibling.firstChild.firstChild.firstChild.firstChild, 0);
+  reset();
+  getTableEditor().getCellDataAt(null, 0, 0,
+                                 cellElementWrapper,
+                                 startRowIndexWrapper, startColumnIndexWrapper,
+                                 rowspanWrapper, colspanWrapper,
+                                 effectiveRowspanWrapper, effectiveColspanWrapper,
+                                 isSelectedWrapper);
+  is(cellElementWrapper.value, editor.firstChild.nextSibling.firstChild.firstChild.firstChild,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return the <td> element when selection is in it");
+  is(startRowIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return 0 for startRowIndex when selection is in the cell");
+  is(startColumnIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return 0 for startColumnIndex when selection is in the cell");
+  is(rowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for rowspan when selection is in the cell");
+  is(colspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for colspan when selection is in the cell");
+  is(effectiveRowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for effectiveRowspan when selection is in the cell");
+  is(effectiveColspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for effectiveColspan when selection is in the cell");
+  is(isSelectedWrapper.value, false,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return false for isSelected when selection is in the cell");
+
+  // Select the cell
+  selection.setBaseAndExtent(editor.firstChild.nextSibling.firstChild.firstChild, 0,
+                             editor.firstChild.nextSibling.firstChild.firstChild, 1);
+  reset();
+  getTableEditor().getCellDataAt(null, 0, 0,
+                                 cellElementWrapper,
+                                 startRowIndexWrapper, startColumnIndexWrapper,
+                                 rowspanWrapper, colspanWrapper,
+                                 effectiveRowspanWrapper, effectiveColspanWrapper,
+                                 isSelectedWrapper);
+  is(cellElementWrapper.value, editor.firstChild.nextSibling.firstChild.firstChild.firstChild,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return the <td> element when it's selected");
+  is(startRowIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return 0 for startRowIndex when the cell is selected");
+  is(startColumnIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return 0 for startColumnIndex when the cell is selected");
+  is(rowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for rowspan when the cell is selected");
+  is(colspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for colspan when the cell is selected");
+  is(effectiveRowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for effectiveRowspan when the cell is selected");
+  is(effectiveColspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for effectiveColspan when the cell is selected");
+  is(isSelectedWrapper.value, true,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return true for isSelected when the cell is selected");
+
+  // Select the <tr>
+  selection.setBaseAndExtent(editor.firstChild.nextSibling.firstChild, 0,
+                             editor.firstChild.nextSibling.firstChild, 1);
+  reset();
+  getTableEditor().getCellDataAt(null, 0, 0,
+                                 cellElementWrapper,
+                                 startRowIndexWrapper, startColumnIndexWrapper,
+                                 rowspanWrapper, colspanWrapper,
+                                 effectiveRowspanWrapper, effectiveColspanWrapper,
+                                 isSelectedWrapper);
+  is(cellElementWrapper.value, editor.firstChild.nextSibling.firstChild.firstChild.firstChild,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return the <td> element when the <tr> is selected");
+  is(startRowIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return 0 for startRowIndex when the <tr> is selected");
+  is(startColumnIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return 0 for startColumnIndex when the <tr> is selected");
+  is(rowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for rowspan when the <tr> is selected");
+  is(colspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for colspan when the <tr> is selected");
+  is(effectiveRowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for effectiveRowspan when the <tr> is selected");
+  is(effectiveColspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for effectiveColspan when the <tr> is selected");
+  is(isSelectedWrapper.value, true,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return true for isSelected when the <tr> is selected");
+
+  // Select the <table>
+  selection.setBaseAndExtent(editor, 1, editor, 2);
+  reset();
+  getTableEditor().getCellDataAt(null, 0, 0,
+                                 cellElementWrapper,
+                                 startRowIndexWrapper, startColumnIndexWrapper,
+                                 rowspanWrapper, colspanWrapper,
+                                 effectiveRowspanWrapper, effectiveColspanWrapper,
+                                 isSelectedWrapper);
+  is(cellElementWrapper.value, editor.firstChild.nextSibling.firstChild.firstChild.firstChild,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return the <td> element when the <table> is selected");
+  is(startRowIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return 0 for startRowIndex when the <table> is selected");
+  is(startColumnIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return 0 for startColumnIndex when the <table> is selected");
+  is(rowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for rowspan when the <table> is selected");
+  is(colspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for colspan when the <table> is selected");
+  is(effectiveRowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for effectiveRowspan when the <table> is selected");
+  is(effectiveColspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return 1 for effectiveColspan when the <table> is selected");
+  is(isSelectedWrapper.value, true,
+     "getTableEditor().getCellDataAt(null, 0, 0) should return true for isSelected when the <table> is selected");
+
+  selection.removeAllRanges();
+  editor.innerHTML = "<table>" +
+                       "<tr><td>cell1-1</td><td>cell1-2</td></tr>" +
+                       "<tr><td>cell2-1</td><td>cell2-2</td></tr>" +
+                       "<tr><td>cell3-1</td><td>cell3-2</td></tr>" +
+                     "</table>";
+  editor.focus();
+  editor.scrollTop; // layout information required.
+  reset();
+  getTableEditor().getCellDataAt(editor.firstChild, 0, 0,
+                                 cellElementWrapper,
+                                 startRowIndexWrapper, startColumnIndexWrapper,
+                                 rowspanWrapper, colspanWrapper,
+                                 effectiveRowspanWrapper, effectiveColspanWrapper,
+                                 isSelectedWrapper);
+  is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return the first <td> element");
+  is(startRowIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startRowIndex");
+  is(startColumnIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startColumnIndex");
+  is(rowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for rowspan");
+  is(colspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for colspan");
+  is(effectiveRowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for effectiveRowspan");
+  is(effectiveColspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for effectiveColspan");
+  is(isSelectedWrapper.value, false,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return false for isSelected");
+
+  reset();
+  getTableEditor().getCellDataAt(editor.firstChild, 0, 1,
+                                 cellElementWrapper,
+                                 startRowIndexWrapper, startColumnIndexWrapper,
+                                 rowspanWrapper, colspanWrapper,
+                                 effectiveRowspanWrapper, effectiveColspanWrapper,
+                                 isSelectedWrapper);
+  is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild.nextSibling,
+     "getTableEditor().getCellDataAt(<table>, 0, 1) should return the second <td> element");
+  is(startRowIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(<table>, 0, 1) should return 0 for startRowIndex");
+  is(startColumnIndexWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for startColumnIndex");
+  is(rowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for rowspan");
+  is(colspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for colspan");
+  is(effectiveRowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for effectiveRowspan");
+  is(effectiveColspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for effectiveColspan");
+  is(isSelectedWrapper.value, false,
+     "getTableEditor().getCellDataAt(<table>, 0, 1) should return false for isSelected");
+
+  try {
+    getTableEditor().getCellDataAt(editor.firstChild, 0, 2,
+                                   cellElementWrapper,
+                                   startRowIndexWrapper, startColumnIndexWrapper,
+                                   rowspanWrapper, colspanWrapper,
+                                   effectiveRowspanWrapper, effectiveColspanWrapper,
+                                   isSelectedWrapper);
+    ok(false, "getTableEditor().getCellDataAt(<table>, 0, 2) should throw exception since column index is out of bounds");
+  } catch (e) {
+    ok(true, "getTableEditor().getCellDataAt(<table>, 0, 2) should throw exception since column index is out of bounds");
+  }
+
+  reset();
+  getTableEditor().getCellDataAt(editor.firstChild, 1, 0,
+                                 cellElementWrapper,
+                                 startRowIndexWrapper, startColumnIndexWrapper,
+                                 rowspanWrapper, colspanWrapper,
+                                 effectiveRowspanWrapper, effectiveColspanWrapper,
+                                 isSelectedWrapper);
+  is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.nextSibling.firstChild,
+     "getTableEditor().getCellDataAt(<table>, 1, 0) should return the first <td> element in the second row");
+  is(startRowIndexWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for startRowIndex");
+  is(startColumnIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(<table>, 1, 0) should return 0 for startColumnIndex");
+  is(rowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for rowspan");
+  is(colspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for colspan");
+  is(effectiveRowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for effectiveRowspan");
+  is(effectiveColspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for effectiveColspan");
+  is(isSelectedWrapper.value, false,
+     "getTableEditor().getCellDataAt(<table>, 1, 0) should return false for isSelected");
+
+  reset();
+  getTableEditor().getCellDataAt(editor.firstChild, 2, 1,
+                                 cellElementWrapper,
+                                 startRowIndexWrapper, startColumnIndexWrapper,
+                                 rowspanWrapper, colspanWrapper,
+                                 effectiveRowspanWrapper, effectiveColspanWrapper,
+                                 isSelectedWrapper);
+  is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.nextSibling.nextSibling.firstChild.nextSibling,
+     "getTableEditor().getCellDataAt(<table>, 2, 1) should return the second <td> element in the last row");
+  is(startRowIndexWrapper.value, 2,
+     "getTableEditor().getCellDataAt(<table>, 2, 1) should return 1 for startRowIndex");
+  is(startColumnIndexWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 2, 1) should return 1 for startColumnIndex");
+  is(rowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 2, 1) should return 1 for rowspan");
+  is(colspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 2, 1) should return 1 for colspan");
+  is(effectiveRowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 2, 1) should return 1 for effectiveRowspan");
+  is(effectiveColspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 2, 1) should return 1 for effectiveColspan");
+  is(isSelectedWrapper.value, false,
+     "getTableEditor().getCellDataAt(<table>, 2, 1) should return false for isSelected");
+
+  try {
+    getTableEditor().getCellDataAt(editor.firstChild, 2, 2,
+                                   cellElementWrapper,
+                                   startRowIndexWrapper, startColumnIndexWrapper,
+                                   rowspanWrapper, colspanWrapper,
+                                   effectiveRowspanWrapper, effectiveColspanWrapper,
+                                   isSelectedWrapper);
+    ok(false, "getTableEditor().getCellDataAt(<table>, 2, 2) should throw exception since column index is out of bounds");
+  } catch (e) {
+    ok(true, "getTableEditor().getCellDataAt(<table>, 2, 2) should throw exception since column index is out of bounds");
+  }
+
+  try {
+    getTableEditor().getCellDataAt(editor.firstChild, 3, 0,
+                                   cellElementWrapper,
+                                   startRowIndexWrapper, startColumnIndexWrapper,
+                                   rowspanWrapper, colspanWrapper,
+                                   effectiveRowspanWrapper, effectiveColspanWrapper,
+                                   isSelectedWrapper);
+    ok(false, "getTableEditor().getCellDataAt(<table>, 3, 0) should throw exception since row index is out of bounds");
+  } catch (e) {
+    ok(true, "getTableEditor().getCellDataAt(<table>, 3, 0) should throw exception since row index is out of bounds");
+  }
+
+  selection.removeAllRanges();
+  editor.innerHTML = "<table>" +
+                       '<tr><td rowspan="3">cell1-1</td><td>cell1-2</td><td>cell1-3</td><td>cell1-4</td></tr>' +
+                       "<tr><td>cell2-2</td></tr>" +
+                       "<tr><td>cell3-2</td><td>cell3-3</td></tr>" +
+                       '<tr><td colspan="3">cell4-1</td><td>cell4-4</td></tr>' +
+                     "</table>";
+  editor.focus();
+  editor.scrollTop; // layout information required.
+  reset();
+  getTableEditor().getCellDataAt(editor.firstChild, 0, 0,
+                                 cellElementWrapper,
+                                 startRowIndexWrapper, startColumnIndexWrapper,
+                                 rowspanWrapper, colspanWrapper,
+                                 effectiveRowspanWrapper, effectiveColspanWrapper,
+                                 isSelectedWrapper);
+  is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return the first <td> element whose rowspan is 3");
+  is(startRowIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startRowIndex (the cell's rowspan is 3)");
+  is(startColumnIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startColumnIndex (the cell's rowspan is 3)");
+  is(rowspanWrapper.value, 3,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return 3 for rowspan (the cell's rowspan is 3)");
+  is(colspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for colspan (the cell's rowspan is 3)");
+  is(effectiveRowspanWrapper.value, 3,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return 3 for effectiveRowspan (the cell's rowspan is 3)");
+  is(effectiveColspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for effectiveColspan (the cell's rowspan is 3)");
+  is(isSelectedWrapper.value, false,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return false for isSelected (the cell's rowspan is 3)");
+
+  reset();
+  getTableEditor().getCellDataAt(editor.firstChild, 1, 0,
+                                 cellElementWrapper,
+                                 startRowIndexWrapper, startColumnIndexWrapper,
+                                 rowspanWrapper, colspanWrapper,
+                                 effectiveRowspanWrapper, effectiveColspanWrapper,
+                                 isSelectedWrapper);
+  is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild,
+     "getTableEditor().getCellDataAt(<table>, 1, 0) should return the first <td> element whose rowspan is 3");
+  is(startRowIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(<table>, 1, 0) should return 0 for startRowIndex (the cell's rowspan is 3)");
+  is(startColumnIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(<table>, 1, 0) should return 0 for startColumnIndex (the cell's rowspan is 3)");
+  is(rowspanWrapper.value, 3,
+     "getTableEditor().getCellDataAt(<table>, 1, 0) should return 3 for rowspan (the cell's rowspan is 3)");
+  is(colspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for colspan (the cell's rowspan is 3)");
+  is(effectiveRowspanWrapper.value, 2,
+     "getTableEditor().getCellDataAt(<table>, 1, 0) should return 2 for effectiveRowspan (the cell's rowspan is 3)");
+  is(effectiveColspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for effectiveColspan (the cell's rowspan is 3)");
+  is(isSelectedWrapper.value, false,
+     "getTableEditor().getCellDataAt(<table>, 1, 0) should return false for isSelected (the cell's rowspan is 3)");
+
+  reset();
+  getTableEditor().getCellDataAt(editor.firstChild, 2, 0,
+                                 cellElementWrapper,
+                                 startRowIndexWrapper, startColumnIndexWrapper,
+                                 rowspanWrapper, colspanWrapper,
+                                 effectiveRowspanWrapper, effectiveColspanWrapper,
+                                 isSelectedWrapper);
+  is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild,
+     "getTableEditor().getCellDataAt(<table>, 2, 0) should return the first <td> element whose rowspan is 3");
+  is(startRowIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(<table>, 2, 0) should return 0 for startRowIndex (the cell's rowspan is 3)");
+  is(startColumnIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(<table>, 2, 0) should return 0 for startColumnIndex (the cell's rowspan is 3)");
+  is(rowspanWrapper.value, 3,
+     "getTableEditor().getCellDataAt(<table>, 2, 0) should return 3 for rowspan (the cell's rowspan is 3)");
+  is(colspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 2, 0) should return 1 for colspan (the cell's rowspan is 3)");
+  is(effectiveRowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 2, 0) should return 1 for effectiveRowspan (the cell's rowspan is 3)");
+  is(effectiveColspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 2, 0) should return 1 for effectiveColspan (the cell's rowspan is 3)");
+  is(isSelectedWrapper.value, false,
+     "getTableEditor().getCellDataAt(<table>, 2, 0) should return false for isSelected (the cell's rowspan is 3)");
+
+  reset();
+  getTableEditor().getCellDataAt(editor.firstChild, 3, 0,
+                                 cellElementWrapper,
+                                 startRowIndexWrapper, startColumnIndexWrapper,
+                                 rowspanWrapper, colspanWrapper,
+                                 effectiveRowspanWrapper, effectiveColspanWrapper,
+                                 isSelectedWrapper);
+  is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.nextSibling.nextSibling.nextSibling.firstChild,
+     "getTableEditor().getCellDataAt(<table>, 3, 0) should return the first <td> element in the last row whose colspan is 3");
+  is(startRowIndexWrapper.value, 3,
+     "getTableEditor().getCellDataAt(<table>, 3, 0) should return 3 for startRowIndex (the cell's colspan is 3)");
+  is(startColumnIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(<table>, 3, 0) should return 0 for startColumnIndex (the cell's colspan is 3)");
+  is(rowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 3, 0) should return 1 for rowspan (the cell's colspan is 3)");
+  is(colspanWrapper.value, 3,
+     "getTableEditor().getCellDataAt(<table>, 3, 0) should return 3 for colspan (the cell's colspan is 3)");
+  is(effectiveRowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 3, 0) should return 1 for effectiveRowspan (the cell's colspan is 3)");
+  is(effectiveColspanWrapper.value, 3,
+     "getTableEditor().getCellDataAt(<table>, 3, 0) should return 3 for effectiveColspan (the cell's colspan is 3)");
+  is(isSelectedWrapper.value, false,
+     "getTableEditor().getCellDataAt(<table>, 3, 0) should return false for isSelected (the cell's colspan is 3)");
+
+  reset();
+  getTableEditor().getCellDataAt(editor.firstChild, 3, 1,
+                                 cellElementWrapper,
+                                 startRowIndexWrapper, startColumnIndexWrapper,
+                                 rowspanWrapper, colspanWrapper,
+                                 effectiveRowspanWrapper, effectiveColspanWrapper,
+                                 isSelectedWrapper);
+  is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.nextSibling.nextSibling.nextSibling.firstChild,
+     "getTableEditor().getCellDataAt(<table>, 3, 1) should return the first <td> element in the last row whose colspan is 3");
+  is(startRowIndexWrapper.value, 3,
+     "getTableEditor().getCellDataAt(<table>, 3, 1) should return 3 for startRowIndex (the cell's colspan is 3)");
+  is(startColumnIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(<table>, 3, 1) should return 0 for startColumnIndex (the cell's colspan is 3)");
+  is(rowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 3, 1) should return 1 for rowspan (the cell's colspan is 3)");
+  is(colspanWrapper.value, 3,
+     "getTableEditor().getCellDataAt(<table>, 3, 1) should return 3 for colspan (the cell's colspan is 3)");
+  is(effectiveRowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 3, 1) should return 1 for effectiveRowspan (the cell's colspan is 3)");
+  is(effectiveColspanWrapper.value, 2,
+     "getTableEditor().getCellDataAt(<table>, 3, 1) should return 2 for effectiveColspan (the cell's colspan is 3)");
+  is(isSelectedWrapper.value, false,
+     "getTableEditor().getCellDataAt(<table>, 3, 1) should return false for isSelected (the cell's colspan is 3)");
+
+  reset();
+  getTableEditor().getCellDataAt(editor.firstChild, 3, 2,
+                                 cellElementWrapper,
+                                 startRowIndexWrapper, startColumnIndexWrapper,
+                                 rowspanWrapper, colspanWrapper,
+                                 effectiveRowspanWrapper, effectiveColspanWrapper,
+                                 isSelectedWrapper);
+  is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.nextSibling.nextSibling.nextSibling.firstChild,
+     "getTableEditor().getCellDataAt(<table>, 3, 2) should return the first <td> element in the last row whose colspan is 3");
+  is(startRowIndexWrapper.value, 3,
+     "getTableEditor().getCellDataAt(<table>, 3, 2) should return 3 for startRowIndex (the cell's colspan is 3)");
+  is(startColumnIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(<table>, 3, 2) should return 0 for startColumnIndex (the cell's colspan is 3)");
+  is(rowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 3, 2) should return 1 for rowspan (the cell's colspan is 3)");
+  is(colspanWrapper.value, 3,
+     "getTableEditor().getCellDataAt(<table>, 3, 2) should return 3 for colspan (the cell's colspan is 3)");
+  is(effectiveRowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 3, 2) should return 1 for effectiveRowspan (the cell's colspan is 3)");
+  is(effectiveColspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 3, 2) should return 1 for effectiveColspan (the cell's colspan is 3)");
+  is(isSelectedWrapper.value, false,
+     "getTableEditor().getCellDataAt(<table>, 3, 2) should return false for isSelected (the cell's colspan is 3)");
+
+  reset();
+  getTableEditor().getCellDataAt(editor.firstChild, 3, 3,
+                                 cellElementWrapper,
+                                 startRowIndexWrapper, startColumnIndexWrapper,
+                                 rowspanWrapper, colspanWrapper,
+                                 effectiveRowspanWrapper, effectiveColspanWrapper,
+                                 isSelectedWrapper);
+  is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.nextSibling.nextSibling.nextSibling.firstChild.nextSibling,
+     "getTableEditor().getCellDataAt(<table>, 3, 3) should return the second <td> element in the last row");
+  is(startRowIndexWrapper.value, 3,
+     "getTableEditor().getCellDataAt(<table>, 3, 3) should return 3 for startRowIndex (right cell of the cell whose colspan is 3)");
+  is(startColumnIndexWrapper.value, 3,
+     "getTableEditor().getCellDataAt(<table>, 3, 3) should return 3 for startColumnIndex (right cell of the cell whose colspan is 3)");
+  is(rowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 3, 3) should return 1 for rowspan (right cell of the cell whose colspan is 3)");
+  is(colspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 3, 3) should return 1 for colspan (right cell of the cell whose colspan is 3)");
+  is(effectiveRowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 3, 3) should return 1 for effectiveRowspan (right cell of the cell whose colspan is 3)");
+  is(effectiveColspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 3, 3) should return 1 for effectiveColspan (right cell of the cell whose colspan is 3)");
+  is(isSelectedWrapper.value, false,
+     "getTableEditor().getCellDataAt(<table>, 3, 3) should return false for isSelected (right cell of the cell whose colspan is 3)");
+
+  try {
+    getTableEditor().getCellDataAt(editor.firstChild, 3, 4,
+                                   cellElementWrapper,
+                                   startRowIndexWrapper, startColumnIndexWrapper,
+                                   rowspanWrapper, colspanWrapper,
+                                   effectiveRowspanWrapper, effectiveColspanWrapper,
+                                   isSelectedWrapper);
+    ok(false, "getTableEditor().getCellDataAt(<table>, 3, 4) should throw exception since column index is out of bounds");
+  } catch (e) {
+    ok(true, "getTableEditor().getCellDataAt(<table>, 3, 4) should throw exception since column index is out of bounds");
+  }
+
+  reset();
+  getTableEditor().getCellDataAt(editor.firstChild, 0, 1,
+                                 cellElementWrapper,
+                                 startRowIndexWrapper, startColumnIndexWrapper,
+                                 rowspanWrapper, colspanWrapper,
+                                 effectiveRowspanWrapper, effectiveColspanWrapper,
+                                 isSelectedWrapper);
+  is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild.nextSibling,
+     "getTableEditor().getCellDataAt(<table>, 0, 1) should return the second <td> element in the first row");
+  is(startRowIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(<table>, 0, 1) should return 0 for startRowIndex (right cell of the cell whose rowspan is 3)");
+  is(startColumnIndexWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for startColumnIndex (right cell of the cell whose rowspan is 3)");
+  is(rowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for rowspan (right cell of the cell whose rowspan is 3)");
+  is(colspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for colspan (right cell of the cell whose rowspan is 3)");
+  is(effectiveRowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for effectiveRowspan (right cell of the cell whose rowspan is 3)");
+  is(effectiveColspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for effectiveColspan (right cell of the cell whose rowspan is 3)");
+  is(isSelectedWrapper.value, false,
+     "getTableEditor().getCellDataAt(<table>, 0, 1) should return false for isSelected (right cell of the cell whose rowspan is 3)");
+
+  reset();
+  getTableEditor().getCellDataAt(editor.firstChild, 1, 1,
+                                 cellElementWrapper,
+                                 startRowIndexWrapper, startColumnIndexWrapper,
+                                 rowspanWrapper, colspanWrapper,
+                                 effectiveRowspanWrapper, effectiveColspanWrapper,
+                                 isSelectedWrapper);
+  is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.nextSibling.firstChild,
+     "getTableEditor().getCellDataAt(<table>, 1, 1) should return the first <td> element in the second row");
+  is(startRowIndexWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 1, 1) should return 1 for startRowIndex (right cell of the cell whose rowspan is 3)");
+  is(startColumnIndexWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 1, 1) should return 1 for startColumnIndex (right cell of the cell whose rowspan is 3)");
+  is(rowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 1, 1) should return 1 for rowspan (right cell of the cell whose rowspan is 3)");
+  is(colspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 1, 1) should return 1 for colspan (right cell of the cell whose rowspan is 3)");
+  is(effectiveRowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 1, 1) should return 1 for effectiveRowspan (right cell of the cell whose rowspan is 3)");
+  is(effectiveColspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 1, 1) should return 1 for effectiveColspan (right cell of the cell whose rowspan is 3)");
+  is(isSelectedWrapper.value, false,
+     "getTableEditor().getCellDataAt(<table>, 1, 1) should return false for isSelected (right cell of the cell whose rowspan is 3)");
+
+  try {
+    getTableEditor().getCellDataAt(editor.firstChild, 1, 2,
+                                   cellElementWrapper,
+                                   startRowIndexWrapper, startColumnIndexWrapper,
+                                   rowspanWrapper, colspanWrapper,
+                                   effectiveRowspanWrapper, effectiveColspanWrapper,
+                                   isSelectedWrapper);
+    ok(false, "getTableEditor().getCellDataAt(<table>, 1, 2) should throw exception since there is no cell due to non-rectangular table");
+  } catch (e) {
+    ok(true, "getTableEditor().getCellDataAt(<table>, 1, 2) should throw exception since there is no cell due to non-rectangular table");
+  }
+
+  selection.removeAllRanges();
+  editor.innerHTML = "<table>" +
+                       '<tr><td rowspan="3" colspan="2">cell1-1</td></tr>' +
+                     "</table>";
+  editor.focus();
+  editor.scrollTop; // layout information required.
+  reset();
+  getTableEditor().getCellDataAt(editor.firstChild, 0, 0,
+                                 cellElementWrapper,
+                                 startRowIndexWrapper, startColumnIndexWrapper,
+                                 rowspanWrapper, colspanWrapper,
+                                 effectiveRowspanWrapper, effectiveColspanWrapper,
+                                 isSelectedWrapper);
+  is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return the first <td> element whose rowspan is 3 and colspan is 2");
+  is(startRowIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startRowIndex (the cell's rowspan is 3 and colspan is 2)");
+  is(startColumnIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startColumnIndex (the cell's rowspan is 3 and colspan is 2)");
+  is(rowspanWrapper.value, 3,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return 3 for rowspan (the cell's rowspan is 3 and colspan is 2)");
+  is(colspanWrapper.value, 2,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return 2 for colspan (the cell's rowspan is 3 and colspan is 2)");
+  // XXX Not sure whether expected behavior or not.
+  todo_is(effectiveRowspanWrapper.value, 3,
+          "getTableEditor().getCellDataAt(<table>, 0, 0) should return 3 for effectiveRowspan (the cell's rowspan is 3 and colspan is 2)");
+  is(effectiveColspanWrapper.value, 2,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return 2 for effectiveColspan (the cell's rowspan is 3 and colspan is 2)");
+  is(isSelectedWrapper.value, false,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return false for isSelected (the cell's rowspan is 3 and colspan is 2)");
+
+  reset();
+  getTableEditor().getCellDataAt(editor.firstChild, 0, 1,
+                                 cellElementWrapper,
+                                 startRowIndexWrapper, startColumnIndexWrapper,
+                                 rowspanWrapper, colspanWrapper,
+                                 effectiveRowspanWrapper, effectiveColspanWrapper,
+                                 isSelectedWrapper);
+  is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild,
+     "getTableEditor().getCellDataAt(<table>, 0, 1) should return the first <td> element whose rowspan is 3 and colspan is 2");
+  is(startRowIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(<table>, 0, 1) should return 0 for startRowIndex (the cell's rowspan is 3 and colspan is 2)");
+  is(startColumnIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(<table>, 0, 1) should return 0 for startColumnIndex (the cell's rowspan is 3 and colspan is 2)");
+  is(rowspanWrapper.value, 3,
+     "getTableEditor().getCellDataAt(<table>, 0, 1) should return 3 for rowspan (the cell's rowspan is 3 and colspan is 2)");
+  is(colspanWrapper.value, 2,
+     "getTableEditor().getCellDataAt(<table>, 0, 1) should return 2 for colspan (the cell's rowspan is 3 and colspan is 2)");
+  // XXX Not sure whether expected behavior or not.
+  todo_is(effectiveRowspanWrapper.value, 3,
+          "getTableEditor().getCellDataAt(<table>, 0, 1) should return 3 for effectiveRowspan (the cell's rowspan is 3 and colspan is 2)");
+  is(effectiveColspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 0, 1) should return 1 for effectiveColspan (the cell's rowspan is 3 and colspan is 2)");
+  is(isSelectedWrapper.value, false,
+     "getTableEditor().getCellDataAt(<table>, 0, 1) should return false for isSelected (the cell's rowspan is 3 and colspan is 2)");
+
+  try {
+    getTableEditor().getCellDataAt(editor.firstChild, 1, 0,
+                                   cellElementWrapper,
+                                   startRowIndexWrapper, startColumnIndexWrapper,
+                                   rowspanWrapper, colspanWrapper,
+                                   effectiveRowspanWrapper, effectiveColspanWrapper,
+                                   isSelectedWrapper);
+    is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild,
+       "getTableEditor().getCellDataAt(<table>, 1, 0) should return the first <td> element whose rowspan is 3 and colspan is 2");
+    is(startRowIndexWrapper.value, 0,
+       "getTableEditor().getCellDataAt(<table>, 1, 0) should return 0 for startRowIndex (the cell's rowspan is 3 and colspan is 2)");
+    is(startColumnIndexWrapper.value, 0,
+       "getTableEditor().getCellDataAt(<table>, 1, 0) should return 0 for startColumnIndex (the cell's rowspan is 3 and colspan is 2)");
+    is(rowspanWrapper.value, 3,
+       "getTableEditor().getCellDataAt(<table>, 1, 0) should return 3 for rowspan (the cell's rowspan is 3 and colspan is 2)");
+    is(colspanWrapper.value, 2,
+       "getTableEditor().getCellDataAt(<table>, 1, 0) should return 2 for colspan (the cell's rowspan is 3 and colspan is 2)");
+    // XXX Not sure whether expected behavior or not.
+    todo_is(effectiveRowspanWrapper.value, 2,
+            "getTableEditor().getCellDataAt(<table>, 1, 0) should return 2 for effectiveRowspan (the cell's rowspan is 3 and colspan is 2)");
+    is(effectiveColspanWrapper.value, 1,
+       "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for effectiveColspan (the cell's rowspan is 3 and colspan is 2)");
+    is(isSelectedWrapper.value, false,
+       "getTableEditor().getCellDataAt(<table>, 1, 0) should return false for isSelected (the cell's rowspan is 3 and colspan is 2)");
+  } catch (e) {
+    todo(false, "getTableEditor().getCellDataAt(<table>, 1, 0) shouldn't throw exception since rowspan expands the table");
+  }
+
+  selection.removeAllRanges();
+  editor.innerHTML = "<table>" +
+                       '<tr><td rowspan="0">cell1-1</td><td>cell1-2</td></tr>' +
+                       "<tr><td>cell2-2</td></tr>" +
+                       "<tr><td>cell3-2</td></tr>" +
+                     "</table>";
+  editor.focus();
+  editor.scrollTop; // layout information required.
+  reset();
+  getTableEditor().getCellDataAt(editor.firstChild, 0, 0,
+                                 cellElementWrapper,
+                                 startRowIndexWrapper, startColumnIndexWrapper,
+                                 rowspanWrapper, colspanWrapper,
+                                 effectiveRowspanWrapper, effectiveColspanWrapper,
+                                 isSelectedWrapper);
+  is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return the first <td> element whose rowspan is 0");
+  is(startRowIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startRowIndex (the cell's rowspan is 0)");
+  is(startColumnIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startColumnIndex (the cell's rowspan is 0)");
+  is(rowspanWrapper.value, 0,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for rowspan (the cell's rowspan is 0)");
+  is(colspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for colspan (the cell's rowspan is 0)");
+  is(effectiveRowspanWrapper.value, 3,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return 3 for effectiveRowspan (the cell's rowspan is 0)");
+  is(effectiveColspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for effectiveColspan (the cell's rowspan is 0)");
+  is(isSelectedWrapper.value, false,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return false for isSelected (the cell's rowspan is 0)");
+
+  reset();
+  getTableEditor().getCellDataAt(editor.firstChild, 1, 0,
+                                 cellElementWrapper,
+                                 startRowIndexWrapper, startColumnIndexWrapper,
+                                 rowspanWrapper, colspanWrapper,
+                                 effectiveRowspanWrapper, effectiveColspanWrapper,
+                                 isSelectedWrapper);
+  is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild,
+     "getTableEditor().getCellDataAt(<table>, 1, 0) should return the first <td> element whose rowspan is 0");
+  is(startRowIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(<table>, 1, 0) should return 0 for startRowIndex (the cell's rowspan is 0)");
+  is(startColumnIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(<table>, 1, 0) should return 0 for startColumnIndex (the cell's rowspan is 0)");
+  is(rowspanWrapper.value, 0,
+     "getTableEditor().getCellDataAt(<table>, 1, 0) should return 0 for rowspan (the cell's rowspan is 0)");
+  is(colspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for colspan (the cell's rowspan is 0)");
+  is(effectiveRowspanWrapper.value, 2,
+     "getTableEditor().getCellDataAt(<table>, 1, 0) should return 3 for effectiveRowspan (the cell's rowspan is 0)");
+  is(effectiveColspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 1, 0) should return 1 for effectiveColspan (the cell's rowspan is 0)");
+  is(isSelectedWrapper.value, false,
+     "getTableEditor().getCellDataAt(<table>, 1, 0) should return false for isSelected (the cell's rowspan is 0)");
+
+  // FYI: colspan must be 1 - 1000, 0 is invalid.
+  selection.removeAllRanges();
+  editor.innerHTML = "<table>" +
+                       '<tr><td colspan="0">cell1-1</td></tr>' +
+                       "<tr><td>cell2-1</td><td>cell2-2</td><td>cell2-3</td></tr>" +
+                     "</table>";
+  editor.focus();
+  editor.scrollTop; // layout information required.
+  reset();
+  getTableEditor().getCellDataAt(editor.firstChild, 0, 0,
+                                 cellElementWrapper,
+                                 startRowIndexWrapper, startColumnIndexWrapper,
+                                 rowspanWrapper, colspanWrapper,
+                                 effectiveRowspanWrapper, effectiveColspanWrapper,
+                                 isSelectedWrapper);
+  is(cellElementWrapper.value, editor.firstChild.firstChild.firstChild.firstChild,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return the first <td> element whose colspan is 0");
+  is(startRowIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startRowIndex (the cell's colspan is 0)");
+  is(startColumnIndexWrapper.value, 0,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return 0 for startColumnIndex (the cell's colspan is 0)");
+  is(rowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for rowspan (the cell's colspan is 0)");
+  is(colspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for colspan (the cell's colspan is 0)");
+  is(effectiveRowspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return 3 for effectiveRowspan (the cell's colspan is 0)");
+  is(effectiveColspanWrapper.value, 1,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return 1 for effectiveColspan (the cell's colspan is 0)");
+  is(isSelectedWrapper.value, false,
+     "getTableEditor().getCellDataAt(<table>, 0, 0) should return false for isSelected (the cell's colspan is 0)");
+
+  try {
+    getTableEditor().getCellDataAt(editor.firstChild, 0, 1,
+                                   cellElementWrapper,
+                                   startRowIndexWrapper, startColumnIndexWrapper,
+                                   rowspanWrapper, colspanWrapper,
+                                   effectiveRowspanWrapper, effectiveColspanWrapper,
+                                   isSelectedWrapper);
+    ok(false, "getTableEditor().getCellDataAt(<table>, 0, 1) should throw exception since there is no cell due to right side of a cell whose colspan is 0");
+  } catch (e) {
+    ok(true, "getTableEditor().getCellDataAt(<table>, 0, 1) should throw exception since there is no cell due to right side of a cell whose colspan is 0");
+  }
+
+  SimpleTest.finish();
+});
+
+function getTableEditor() {
+  var editingSession = SpecialPowers.wrap(window).docShell.editingSession;
+  return editingSession.getEditorForWindow(window).QueryInterface(SpecialPowers.Ci.nsITableEditor);
+}
+
+</script>
+</body>
+
+</html>
--- a/editor/nsITableEditor.idl
+++ b/editor/nsITableEditor.idl
@@ -280,47 +280,67 @@ interface nsITableEditor : nsISupports
    *                            element, throwing an exception.
    *                            If aTableElement is null and anchor of Selection
    *                            is not in any <table> element, throwing an
    *                            exception.
    */
   Element getCellAt(in Element aTableElement,
                     in long aRowIndex, in long aColumnIndex);
 
-  /** Get a cell at cellmap grid coordinates and associated data
-    * A cell that spans across multiple cellmap locations will
-    *   be returned multiple times, once for each location it occupies
-    * Examine the returned aStartRowIndex and aStartColIndex to see
-    *   if it is in the same layout column or layout row:
-    *   A "layout row" is all cells sharing the same top edge
-    *   A "layout column" is all cells sharing the same left edge
-    *   This is important to determine what to do when inserting or deleting a column or row
-    *
-    *  @param aTable                   A table in the document
-    *  @param aRowIndex, aColIndex     The 0-based cellmap indexes
-    * returns values:
-    *  @param aCell                    The cell at this cellmap location
-    *  @param aStartRowIndex           The row index where cell starts
-    *  @param aStartColIndex           The col index where cell starts
-    *  @param aRowSpan                 May be 0 (to span down entire table) or number of cells spanned
-    *  @param aColSpan                 May be 0 (to span across entire table) or number of cells spanned
-    *  @param aActualRowSpan           The actual number of cellmap locations (rows) spanned by the cell
-    *  @param aActualColSpan           The actual number of cellmap locations (columns) spanned by the cell
-    *  @param aIsSelected
-    *  @param
-    *
-    * (in C++ returns: NS_EDITOR_ELEMENT_NOT_FOUND if an element is not found
-    *  passes NS_SUCCEEDED macro)
-    */
-  void getCellDataAt(in Element aTable,
-                     in long  aRowIndex, in long  aColIndex,
-                     out Element aCell,
-                     out long  aStartRowIndex, out long  aStartColIndex,
-                     out long  aRowSpan, out long  aColSpan,
-                     out long  aActualRowSpan, out long  aActualColSpan,
+  /**
+   * Get cell element and its various information from <table> element and
+   * indexes in it.  If aTableElement is null, this looks for an ancestor
+   * <table> element of anchor of Selection.  If there is no <table> element
+   * at that point, this throws exception.  Note that this requires layout
+   * information.  So, you need to flush the layout after changing the DOM
+   * tree.
+   * If there is no cell element at the indexes, this throws exception.
+   * XXX Perhaps, this is wrong behavior, this should return null without
+   *     exception since the caller cannot distinguish whether the exception
+   *     is caused by "not found" or other unexpected situation.
+   *
+   * @param aTableElement       A <table> element.  If this is null, this
+   *                            uses ancestor of anchor of Selection.
+   * @param aRowIndex           Row index in aTableElement.  Starting from 0.
+   * @param aColumnIndex        Column index in aTableElement.  Starting from
+   *                            0.
+   * @param aCellElement        [OUT] The cell element at the indexes.
+   * @param aStartRowIndex      [OUT] First row index which contains
+   *                            aCellElement.  E.g., if the cell's rowspan is
+   *                            not 1, this returns its first row index.
+   *                            I.e., this can be smaller than aRowIndex.
+   * @param aStartColumnIndex   [OUT] First column index which contains the
+   *                            aCellElement.  E.g., if the cell's colspan is
+   *                            larger than 1, this returns its first column
+   *                            index.  I.e., this can be smaller than
+   *                            aColumIndex.
+   * @param aRowSpan            [OUT] rowspan attribute value in most cases.
+   *                            If the specified value is invalid, this
+   *                            returns 1.  Only when the document is written
+   *                            in HTML5 or later, this can be 0.
+   * @param aColSpan            [OUT] colspan attribute value in most cases.
+   *                            If the specified value is invalid, this
+   *                            returns 1.
+   * @param aEffectiveRowSpan   [OUT] Effective rowspan value at aRowIndex.
+   *                            This is same as:
+   *                              aRowSpan - (aRowIndex - aStartRowIndex)
+   * @param aEffectiveColSpan   [OUT] Effective colspan value at aColumnIndex.
+   *                            This is same as:
+   *                              aColSpan - (aColumnIndex - aStartColumnIndex)
+   * @param aIsSelected         [OUT] Returns true if aCellElement or its
+   *                            <tr> or <table> element is selected.
+   *                            Otherwise, e.g., aCellElement just contains
+   *                            selection range, returns false.
+   */
+  void getCellDataAt(in Element aTableElement,
+                     in long aRowIndex, in long aColumnIndex,
+                     out Element aCellElement,
+                     out long aStartRowIndex, out long aStartColumnIndex,
+                     out long aRowSpan, out long aColSpan,
+                     out long aEffectiveRowSpan, out long aEffectiveColSpan,
                      out boolean aIsSelected);
 
   /**
    * getFirstRow() returns first <tr> element in a <table> element.
    *
    * @param aTableOrElementInTable  If a <table> element, returns its first
    *                                <tr> element.
    *                                If another element, looks for nearest
--- a/toolkit/components/satchel/test/test_form_submission.html
+++ b/toolkit/components/satchel/test/test_form_submission.html
@@ -119,17 +119,17 @@
         input.type = "text";
         input.name = "test" + (i + 1);
         form.appendChild(input);
       }
     </script>
     <button type="submit">Submit</button>
   </form>
 
-  <!-- input with sensitive data (9 digit credit card number) -->
+  <!-- input with sensitive data (19 digit credit card number) -->
   <form id="form17" onsubmit="return checkSubmit(17)">
     <input type="text" name="test1">
     <button type="submit">Submit</button>
   </form>
 
   <!-- input with sensitive data (16 digit hyphenated credit card number) -->
   <form id="form18" onsubmit="return checkSubmit(18)">
     <input type="text" name="test1">
@@ -317,29 +317,29 @@ function startTest() {
   for (let i = 0; i != testData.length; i++) {
     $_(15, "test" + (i + 1)).value = testData[i];
   }
 
   testData = ccNumbers.valid15;
   for (let i = 0; i != testData.length; i++) {
     $_(16, "test" + (i + 1)).value = testData[i];
   }
-  $_(17, "test1").value = "001064088";
+  $_(17, "test1").value = "6799990100000000019";
   $_(18, "test1").value = "0000-0000-0080-4609";
   $_(19, "test1").value = "0000 0000 0222 331";
   $_(20, "test1").value = "dontSaveThis";
   $_(21, "test1").value = "dontSaveThis";
   $_(22, "searchbar-history").value = "dontSaveThis";
 
   $_(101, "test1").value = "savedValue";
   $_(102, "test2").value = "savedValue";
   $_(103, "test3").value = "savedValue";
   $_(104, "test4").value = " trimTrailingAndLeadingSpace ";
   $_(105, "test5").value = "\t trimTrailingAndLeadingWhitespace\t ";
-  $_(106, "test6").value = "00000000109181";
+  $_(106, "test6").value = "55555555555544445553"; // passes luhn but too long
 
   testData = ccNumbers.invalid16;
   for (let i = 0; i != testData.length; i++) {
     $_(107, "test7_" + (i + 1)).value = testData[i];
   }
 
   testData = ccNumbers.invalid15;
   for (let i = 0; i != testData.length; i++) {
@@ -407,17 +407,17 @@ function checkSubmit(formNum) {
       checkForSave("test4", "trimTrailingAndLeadingSpace",
                    "checking saved value is trimmed on both sides");
       break;
     case 105:
       checkForSave("test5", "trimTrailingAndLeadingWhitespace",
                    "checking saved value is trimmed on both sides");
       break;
     case 106:
-      checkForSave("test6", "00000000109181", "checking saved value");
+      checkForSave("test6", "55555555555544445553", "checking saved value");
       break;
     case 107:
       for (let i = 0; i != ccNumbers.invalid16.length; i++) {
         checkForSave("test7_" + (i + 1), ccNumbers.invalid16[i], "checking saved value");
       }
       break;
     case 108:
       for (let i = 0; i != ccNumbers.invalid15.length; i++) {
--- a/toolkit/modules/CreditCard.jsm
+++ b/toolkit/modules/CreditCard.jsm
@@ -103,44 +103,46 @@ class CreditCard {
   get number() {
     return this._number;
   }
 
   set number(value) {
     if (value) {
       let normalizedNumber = value.replace(/[-\s]/g, "");
       // Based on the information on wiki[1], the shortest valid length should be
-      // 9 digits (Canadian SIN).
-      // [1] https://en.wikipedia.org/wiki/Social_Insurance_Number
-      normalizedNumber = normalizedNumber.match(/^\d{9,}$/) ?
+      // 12 digits (Maestro).
+      // [1] https://en.wikipedia.org/wiki/Payment_card_number
+      normalizedNumber = normalizedNumber.match(/^\d{12,}$/) ?
         normalizedNumber : null;
       this._number = normalizedNumber;
     }
   }
 
   get network() {
     return this._network;
   }
 
   set network(value) {
     this._network = value || undefined;
   }
 
   // Implements the Luhn checksum algorithm as described at
   // http://wikipedia.org/wiki/Luhn_algorithm
+  // Number digit lengths vary with network, but should fall within 12-19 range. [2]
+  // More details at https://en.wikipedia.org/wiki/Payment_card_number
   isValidNumber() {
     if (!this._number) {
       return false;
     }
 
     // Remove dashes and whitespace
     let number = this._number.replace(/[\-\s]/g, "");
 
     let len = number.length;
-    if (len != 9 && len != 15 && len != 16) {
+    if (len < 12 || len > 19) {
       return false;
     }
 
     if (!/^\d+$/.test(number)) {
       return false;
     }
 
     let total = 0;
--- a/toolkit/modules/tests/xpcshell/test_CreditCard.js
+++ b/toolkit/modules/tests/xpcshell/test_CreditCard.js
@@ -10,30 +10,52 @@ add_task(function isValidNumber() {
     if (shouldPass) {
       ok(CreditCard.isValidNumber(number), `${number} should be considered valid`);
     } else {
       ok(!CreditCard.isValidNumber(number), `${number} should not be considered valid`);
     }
   }
 
   testValid("0000000000000000", true);
+
+  testValid("41111111112", false); // passes Luhn but too short
+  testValid("4111-1111-112", false); // passes Luhn but too short
+  testValid("55555555555544440018", false); // passes Luhn but too long
+  testValid("5555 5555 5555 4444 0018", false); // passes Luhn but too long
+
   testValid("4929001587121045", true);
   testValid("5103059495477870", true);
   testValid("6011029476355493", true);
   testValid("3589993783099582", true);
   testValid("5415425865751454", true);
-  if (CreditCard.isValidNumber("30190729470495")) {
-    ok(false, "todo: 14-digit numbers (Diners Club) aren't supported by isValidNumber yet");
-  }
-  if (CreditCard.isValidNumber("36333851788250")) {
-    ok(false, "todo: 14-digit numbers (Diners Club) aren't supported by isValidNumber yet");
-  }
-  if (CreditCard.isValidNumber("3532596776688495393")) {
-    ok(false, "todo: 19-digit numbers (JCB, Discover, Maestro) could have 16-19 digits");
-  }
+
+  testValid("378282246310005", true); // American Express test number
+  testValid("371449635398431", true); // American Express test number
+  testValid("378734493671000", true); // American Express Corporate test number
+  testValid("5610591081018250", true); // Australian BankCard test number
+  testValid("6759649826438453", true); // Maestro test number
+  testValid("6799990100000000019", true); // 19 digit Maestro test number
+  testValid("6799-9901-0000-0000019", true); // 19 digit Maestro test number
+  testValid("30569309025904", true); // 14 digit Diners Club test number
+  testValid("38520000023237", true); // 14 digit Diners Club test number
+  testValid("6011111111111117", true); // Discover test number
+  testValid("6011000990139424", true); // Discover test number
+  testValid("3530111333300000", true); // JCB test number
+  testValid("3566002020360505", true); // JCB test number
+  testValid("3532596776688495393", true); // 19-digit JCB number. JCB, Discover, Maestro could have 16-19 digits
+  testValid("3532 5967 7668 8495393", true); // 19-digit JCB number. JCB, Discover, Maestro could have 16-19 digits
+  testValid("5555555555554444", true); // MasterCard test number
+  testValid("5105105105105100", true); // MasterCard test number
+  testValid("2221000000000009", true); // 2-series MasterCard test number
+  testValid("4111111111111111", true); // Visa test number
+  testValid("4012888888881881", true); // Visa test number
+  testValid("4222222222222", true); // 13 digit Visa test number
+  testValid("4222 2222 22222", true); // 13 digit Visa test number
+  testValid("4035 5010 0000 0008", true); // Visadebit/Cartebancaire test number
+
   testValid("5038146897157463", true);
   testValid("4026313395502338", true);
   testValid("6387060366272981", true);
   testValid("474915027480942", true);
   testValid("924894781317325", true);
   testValid("714816113937185", true);
   testValid("790466087343106", true);
   testValid("474320195408363", true);
@@ -50,17 +72,18 @@ add_task(function isValidNumber() {
   testValid("4302068493801686", true);
   testValid("2721398408985465", true);
   testValid("6160334316984331", true);
   testValid("8643619970075142", true);
   testValid("0218246069710785", true);
   testValid("0000-0000-0080-4609", true);
   testValid("0000 0000 0222 331", true);
   testValid("344060747836806", true);
-  testValid("001064088", true);
+  testValid("001064088", false); // too short
+  testValid("00-10-64-088", false); // still too short
   testValid("4929001587121046", false);
   testValid("5103059495477876", false);
   testValid("6011029476355494", false);
   testValid("3589993783099581", false);
   testValid("5415425865751455", false);
   testValid("5038146897157462", false);
   testValid("4026313395502336", false);
   testValid("6387060366272980", false);
@@ -112,16 +135,17 @@ add_task(function test_maskNumber() {
   }
   testMask("0000000000000000", "**** 0000");
   testMask("4929001587121045", "**** 1045");
   testMask("5103059495477870", "**** 7870");
   testMask("6011029476355493", "**** 5493");
   testMask("3589993783099582", "**** 9582");
   testMask("5415425865751454", "**** 1454");
   testMask("344060747836806", "**** 6806");
+  testMask("6799990100000000019", "**** 0019");
   Assert.throws(() => (new CreditCard({number: "1234"})).maskedNumber,
     /Invalid credit card number/,
     "Four or less numbers should throw when retrieving the maskedNumber");
 });
 
 add_task(function test_longMaskedNumber() {
   function testMask(number, expected) {
     let card = new CreditCard({number});
@@ -130,16 +154,18 @@ add_task(function test_longMaskedNumber(
   }
   testMask("0000000000000000", "************0000");
   testMask("4929001587121045", "************1045");
   testMask("5103059495477870", "************7870");
   testMask("6011029476355493", "************5493");
   testMask("3589993783099582", "************9582");
   testMask("5415425865751454", "************1454");
   testMask("344060747836806", "***********6806");
+  testMask("6799990100000000019", "***************0019");
+
   Assert.throws(() => (new CreditCard({number: "1234"})).longMaskedNumber,
     /Invalid credit card number/,
     "Four or less numbers should throw when retrieving the maskedNumber");
 });
 
 add_task(function test_isValid() {
   function testValid(number, expirationMonth, expirationYear, shouldPass, message) {
     let card = new CreditCard({