Bug 1028497 - Part 26: Tests. r=jdaggett
authorCameron McCormack <cam@mcc.id.au>
Thu, 02 Oct 2014 12:32:09 +1000
changeset 231554 7e52db037dd484fc5f24a5b1e74c0cd33aae9ba8
parent 231553 6d287b9a4126d61df2124b269a4999700e71c86f
child 231555 9463ab08610e0da0b271ad4d058f514075e2cb63
push id4187
push userbhearsum@mozilla.com
push dateFri, 28 Nov 2014 15:29:12 +0000
treeherdermozilla-beta@f23cc6a30c11 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjdaggett
bugs1028497
milestone35.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1028497 - Part 26: Tests. r=jdaggett
layout/style/test/BitPattern.woff
layout/style/test/mochitest.ini
layout/style/test/neverending_font_load.sjs
layout/style/test/neverending_stylesheet_load.sjs
layout/style/test/test_font_loading_api.html
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..e4e8244057758f6b888ce985643f59b54f7a2560
GIT binary patch
literal 6248
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
P5-kz?pA-6I0P6n$+<c~!
--- a/layout/style/test/mochitest.ini
+++ b/layout/style/test/mochitest.ini
@@ -6,16 +6,18 @@ support-files =
   ccd-standards.html
   css_properties.js
   descriptor_database.js
   empty.html
   media_queries_dynamic_xbl_binding.xml
   media_queries_dynamic_xbl_iframe.html
   media_queries_dynamic_xbl_style.css
   media_queries_iframe.html
+  neverending_font_load.sjs
+  neverending_stylesheet_load.sjs
   post-redirect-1.css
   post-redirect-2.css
   post-redirect-3.css
   property_database.js
   redirect.sjs
   style_attribute_tests.js
   unstyled.css
   unstyled-frame.css
@@ -131,16 +133,18 @@ skip-if = toolkit == 'android' #bug 5366
 [test_flexbox_layout.html]
 support-files = flexbox_layout_testcases.js
 [test_flexbox_min_size_auto.html]
 [test_flexbox_order.html]
 [test_flexbox_order_table.html]
 [test_font_face_parser.html]
 [test_font_family_parsing.html]
 [test_font_feature_values_parsing.html]
+[test_font_loading_api.html]
+support-files = BitPattern.woff
 [test_garbage_at_end_of_declarations.html]
 [test_grid_item_shorthands.html]
 [test_grid_container_shorthands.html]
 [test_grid_shorthand_serialization.html]
 [test_group_insertRule.html]
 [test_html_attribute_computed_values.html]
 [test_ident_escaping.html]
 [test_inherit_computation.html]
new file mode 100644
--- /dev/null
+++ b/layout/style/test/neverending_font_load.sjs
@@ -0,0 +1,6 @@
+function handleRequest(request, response)
+{
+  response.processAsync();
+  response.setHeader("Content-Type", "application/octet-stream", false);
+  response.write("");
+}
new file mode 100644
--- /dev/null
+++ b/layout/style/test/neverending_stylesheet_load.sjs
@@ -0,0 +1,6 @@
+function handleRequest(request, response)
+{
+  response.processAsync();
+  response.setHeader("Content-Type", "text/css", false);
+  response.write("");
+}
new file mode 100644
--- /dev/null
+++ b/layout/style/test/test_font_loading_api.html
@@ -0,0 +1,882 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<title>Test for the CSS Font Loading API</title>
+<script src=/tests/SimpleTest/SimpleTest.js></script>
+<link rel=stylesheet type=text/css href=/tests/SimpleTest/test.css>
+
+<script src=descriptor_database.js></script>
+
+<script>
+// Map of FontFace descriptor attribute names to @font-face rule descriptor
+// names.
+var descriptorNames = {
+  style: "font-style",
+  weight: "font-weight",
+  stretch: "font-stretch",
+  unicodeRange: "unicode-range",
+  variant: "font-variant",
+  featureSettings: "font-feature-settings"
+};
+
+// Default values for the FontFace descriptor attributes other than family, as
+// Gecko currently serializes them.
+var defaultValues = {
+  style: "normal",
+  weight: "normal",
+  stretch: "normal",
+  unicodeRange: "U+0000-10FFFF",
+  variant: "normal",
+  featureSettings: "normal"
+};
+
+// Non-default values for the FontFace descriptor attributes other than family
+// along with how Gecko currently serializes them.  Each value is chosen to be
+// differeny from the default value and also has a different serialized form.
+var nonDefaultValues = {
+  style: ["Italic", "italic"],
+  weight: ["Bold", "bold"],
+  stretch: ["Ultra-Condensed", "ultra-condensed"],
+  unicodeRange: ["U+3??", "U+0300-03FF"],
+  variant: ["Small-Caps", "small-caps"],
+  featureSettings: ["'dlig' on", "\"dlig\""]
+};
+
+// Invalid values for the FontFace descriptor attributes other than family.
+var invalidValues = {
+  style: "initial",
+  weight: "bolder",
+  stretch: "wider",
+  unicodeRange: "U+1????-2????",
+  variant: "inherit",
+  featureSettings: "dlig"
+};
+
+// Invalid font family names.
+var invalidFontFamilyNames = [
+  "", "\"\"", "sans-serif", "A, B", "inherit"
+];
+
+// Will hold an ArrayBuffer containing a valid font.
+var fontData;
+
+var queue = Promise.resolve();
+
+function is_resolved_with(aPromise, aExpectedValue, aDescription, aTestID) {
+  // This assumes that all Promise tasks come from the task source.
+  var handled = false;
+  return new Promise(function(aResolve, aReject) {
+    aPromise.then(function(aValue) {
+      if (!handled) {
+        handled = true;
+        is(aValue, aExpectedValue, aDescription + " should be resolved with the expected value " + aTestID);
+        aResolve();
+      }
+    }, function(aError) {
+      if (!handled) {
+        handled = true;
+        ok(false, aDescription + " should be resolved; instead it was rejected with " + aError + " " + aTestID);
+        aResolve();
+      }
+    });
+    Promise.resolve().then(function() {
+      if (!handled) {
+        handled = true;
+        ok(false, aDescription + " should be resolved; instead it is pending " + aTestID);
+        aResolve();
+      }
+    });
+  });
+}
+
+function is_pending(aPromise, aDescription, aTestID) {
+  // This assumes that all Promise tasks come from the task source.
+  var handled = false;
+  return new Promise(function(aResolve, aReject) {
+    aPromise.then(function(aValue) {
+      if (!handled) {
+        handled = true;
+        ok(false, aDescription + " should be pending; instead it was resolved with " + aValue + " " + aTestID);
+        aResolve();
+      }
+    }, function(aError) {
+      if (!handled) {
+        handled = true;
+        ok(false, aDescription + " should be pending; instead it was rejected with " + aError + " " + aTestID);
+        aResolve();
+      }
+    });
+    Promise.resolve().then(function() {
+      if (!handled) {
+        handled = true;
+        ok(true, aDescription + " should be pending " + aTestID);
+        aResolve();
+      }
+    });
+  });
+}
+
+function fetchAsArrayBuffer(aURL) {
+  var xhr;
+  return new Promise(function(aResolve, aReject) {
+    var xhr = new XMLHttpRequest();
+    xhr.open("GET", aURL);
+    xhr.responseType = "arraybuffer";
+    xhr.onreadystatechange = function(evt) {
+      if (xhr.readyState == 4) {
+        if (xhr.status >= 200 && xhr.status <= 299) {
+          aResolve(xhr.response);
+        } else {
+          aReject(new Error("Error fetching file " + aURL + ", status " + xhr.status));
+        }
+      }
+    };
+    xhr.send();
+  });
+}
+
+function setTimeoutZero() {
+  return new Promise(function(aResolve, aReject) {
+    setTimeout(aResolve, 0);
+  });
+}
+
+function awaitRefresh() {
+  function awaitOneRefresh() {
+    return new Promise(function(aResolve, aReject) {
+      requestAnimationFrame(aResolve);
+    });
+  }
+
+  return awaitOneRefresh().then(awaitOneRefresh);
+}
+
+function runTest() {
+  SimpleTest.waitForExplicitFinish();
+
+  queue = queue.then(function() {
+
+    // First, initialize fontData.
+    return fetchAsArrayBuffer("BitPattern.woff")
+             .then(function(aResult) { fontData = aResult; });
+
+  }).then(function() {
+
+    // (TEST 1) Some miscellaneous tests for FontFaceSet and FontFace.
+    ok(window.FontFaceSet, "FontFaceSet interface object should be present (TEST 1)");
+    is(Object.getPrototypeOf(FontFaceSet.prototype), EventTarget.prototype, "FontFaceSet should inherit from EventTarget (TEST 1)");
+    ok(document.fonts instanceof FontFaceSet, "document.fonts should be a a FontFaceSet (TEST 1)");
+    ok(window.FontFace, "FontFace interface object should be present (TEST 1)");
+    is(Object.getPrototypeOf(FontFace.prototype), Object.prototype, "FontFace should inherit from Object (TEST 1)");
+
+    // (TEST 2) Some miscellaneous tests for CSSFontFaceLoadEvent.
+    ok(window.CSSFontFaceLoadEvent, "CSSFontFaceLoadEvent interface object should be present (TEST 2)");
+    is(Object.getPrototypeOf(CSSFontFaceLoadEvent.prototype), Event.prototype, "CSSFontFaceLoadEvent should inherit from Event (TEST 2)");
+
+  }).then(function() {
+
+    // (TEST 3) Test that document.fonts.ready starts out resolved with the
+    // FontFaceSet.
+    return is_resolved_with(document.fonts.ready, document.fonts, "initial value of document.fonts", "(TEST 3)");
+
+  }).then(function() {
+
+    // (TEST 4) Test that document.fonts in this test document starts out with no
+    // FontFace objects in it.
+    is(Array.from(document.fonts).length, 0, "initial number of FontFace objects in document.fonts (TEST 4)");
+
+    // (TEST 5) Test that document.fonts.status starts off as loaded.
+    is(document.fonts.status, "loaded", "initial value of document.fonts.status (TEST 5)");
+
+    // (TEST 6) Test initial value of FontFace.status when a url() source is
+    // used.
+    is(new FontFace("test", "url(x)").status, "unloaded", "initial value of FontFace.status when a url() source is used (TEST 6)");
+
+    // (TEST 7) Test initial value of FontFace.status when an invalid
+    // ArrayBuffer source is used.
+    is(new FontFace("test", new ArrayBuffer(0)).status, "unloaded", "initial value of FontFace.status when an invalid ArrayBuffer source is used (TEST 7)");
+
+    // (TEST 8) Test initial value of FontFace.status when a valid ArrayBuffer
+    // source is used.
+    is(new FontFace("test", fontData).status, "unloaded", "initial value of FontFace.status when a valid ArrayBuffer source is used (TEST 8)");
+
+    // (TEST 9) Test initial value of FontFace.loaded when an invalid url()
+    // source is used.
+    return is_pending(new FontFace("test", "").loaded, "initial value of FontFace.loaded when an invalid url() source is used", "(TEST 9)");
+
+  }).then(function() {
+
+    // (TEST 10) Test initial value of FontFace.loaded when a valid url()
+    // source is used.
+    return is_pending(new FontFace("test", "url(x)").loaded, "initial value of FontFace.loaded when a valid url() source is used", "(TEST 10)");
+
+  }).then(function() {
+
+    // (TEST 11) Test initial value of FontFace.loaded when an invalid
+    // ArrayBuffer source is used.
+    return is_pending(new FontFace("test", new ArrayBuffer(0)).loaded, "initial value of FontFace.loaded when an invalid ArrayBuffer source is used", "(TEST 11)");
+
+  }).then(function() {
+
+    // (TEST 12) Test initial value of FontFace.loaded when a valid ArrayBuffer
+    // source is used.
+    return is_pending(new FontFace("test", fontData).loaded, "initial value of FontFace.loaded when a valid ArrayBuffer source is used", "(TEST 12)");
+
+  }).then(function() {
+
+    // (TEST 13) Test initial values of the descriptor attributes on FontFace
+    // objects.
+    var face = new FontFace("test", fontData);
+    // XXX Spec issue: what values do the descriptor attributes have before the
+    // constructor's dictionary argument is parsed?
+    for (var desc in defaultValues) {
+      is(face[desc], defaultValues[desc], "initial value of FontFace." + desc + " (TEST 13)");
+    }
+
+    // (TEST 14) Test default values of the FontFaceDescriptors dictionary.
+    return face.loaded.then(function() {
+      for (var desc in defaultValues) {
+        is(face[desc], defaultValues[desc], "default value of FontFace." + desc + " (TEST 14)");
+      }
+    }, function(aError) {
+      ok(false, "FontFace should have loaded succesfully (TEST 14)");
+    });
+
+  }).then(function() {
+
+    // (TEST 15) Test passing non-default descriptor values to the FontFace
+    // constructor.
+    var descriptorTests = Promise.resolve();
+    Object.keys(nonDefaultValues).forEach(function(aDesc) {
+      descriptorTests = descriptorTests.then(function() {
+        var init = {};
+        init[aDesc] = nonDefaultValues[aDesc][0];
+        var face = new FontFace("test", fontData, init);
+        var ok_todo = aDesc == "variant" ? todo : ok;
+        ok_todo(face[aDesc] == nonDefaultValues[aDesc][1], "specified valid non-default value of FontFace." + aDesc + " immediately after construction (TEST 15)");
+        return face.loaded.then(function() {
+          ok_todo(face[aDesc] == nonDefaultValues[aDesc][1], "specified valid non-default value of FontFace." + aDesc + " (TEST 15)");
+        }, function(aError) {
+          ok(false, "FontFace should have loaded succesfully (TEST 15)");
+        });
+      });
+    });
+    return descriptorTests;
+
+  }).then(function() {
+
+    // (TEST 16) Test passing invalid descriptor values to the FontFace
+    // constructor.
+    var descriptorTests = Promise.resolve();
+    Object.keys(invalidValues).forEach(function(aDesc) {
+      descriptorTests = descriptorTests.then(function() {
+        var init = {};
+        init[aDesc] = invalidValues[aDesc];
+        var face = new FontFace("test", fontData, init);
+        var ok_todo = aDesc == "variant" ? todo : ok;
+        ok_todo(face.status == "error", "FontFace should be error immediately after construction with invalid value of FontFace." + aDesc + " (TEST 16)");
+        return face.loaded.then(function() {
+          ok_todo(false, "FontFace should not load after invalid value of FontFace." + aDesc + " specified (TEST 16)");
+        }, function(aError) {
+          ok(true, "FontFace should not load after invalid value of FontFace." + aDesc + " specified (TEST 16)");
+          is(aError.name, "SyntaxError", "FontFace.loaded with invalid value of FontFace." + aDesc + " should be rejected with a SyntaxError (TEST 16)");
+        });
+      });
+    });
+    return descriptorTests;
+
+  }).then(function() {
+
+    // (TEST 17) Test passing an invalid font family name to the FontFace
+    // constructor.
+    var familyTests = Promise.resolve();
+    invalidFontFamilyNames.forEach(function(aFamilyName) {
+      familyTests = familyTests.then(function() {
+        var face = new FontFace(aFamilyName, fontData);
+        is(face.status, "error", "FontFace should be error immediately after construction with invalid family name " + aFamilyName + " (TEST 17)");
+        return face.loaded.then(function() {
+          ok(false, "FontFace should not load after invalid family name " + aFamilyName + " specified (TEST 17)");
+        }, function(aError) {
+          ok(true, "FontFace should not load after invalid family name " + aFamilyName + " specified (TEST 17)");
+          is(aError.name, "SyntaxError", "FontFace.loaded with invalid family name " + aFamilyName + " should be rejected with a SyntaxError (TEST 17)");
+        });
+      });
+    });
+    return familyTests;
+
+  }).then(function() {
+
+    // (TEST 18) Test passing valid url() source strings to the FontFace
+    // constructor.
+    var srcTests = Promise.resolve();
+    gCSSFontFaceDescriptors.src.values.forEach(function(aSrc) {
+      srcTests = srcTests.then(function() {
+        var face = new FontFace("test", aSrc);
+        return face.load().then(function() {
+          ok(true, "FontFace should load with valid url() src " + aSrc + " (TEST 18)");
+        }, function(aError) {
+          is(aError.name, "NetworkError", "FontFace had NetworkError when loading with valid url() src " + aSrc + " (TEST 18)");
+        });
+      });
+    });
+    return srcTests;
+
+  }).then(function() {
+
+    // (TEST 19) Test passing invalid url() source strings to the FontFace
+    // constructor.
+    var srcTests = Promise.resolve();
+    gCSSFontFaceDescriptors.src.invalid_values.forEach(function(aSrc) {
+      srcTests = srcTests.then(function() {
+        var face = new FontFace("test", aSrc);
+        return face.load().then(function() {
+          ok(false, "FontFace should not load with invalid url() src " + aSrc + " (TEST 19)");
+        }, function(aError) {
+          is(aError.name, "SyntaxError", "FontFace.ready should have been rejected with a SyntaxError when loaded with an invalid url() src " + aSrc + " (TEST 19)");
+        });
+      });
+    });
+    return srcTests;
+
+  }).then(function() {
+
+    // (TEST 20) Test that the status of a FontFace constructed with a valid
+    // ArrayBuffer source eventually becomes "loaded".
+    var face = new FontFace("test", fontData);
+    return face.loaded.then(function() {
+      is(face.status, "loaded", "status of FontFace constructed with a valid ArrayBuffer source should eventually be \"loaded\" (TEST 20)");
+    }, function(aError) {
+      ok(false, "FontFace constructed with a valid ArrayBuffer should eventually load (TEST 20)");
+    });
+
+  }).then(function() {
+
+    // (TEST 21) Test that the status of a FontFace constructed with an invalid
+    // ArrayBuffer source eventually becomes "error".
+    var face = new FontFace("test", new ArrayBuffer(0));
+    return face.loaded.then(function() {
+      ok(false, "FontFace constructed with an invalid ArrayBuffer should not load (TEST 21)");
+    }, function(aError) {
+      is(aError.name, "SyntaxError", "loaded of FontFace constructed with an invalid ArrayBuffer source should be rejected with TypeError (TEST 21)");
+      is(face.status, "error", "status of FontFace constructed with an invalid ArrayBuffer source should eventually be \"error\" (TEST 21)");
+    });
+
+  }).then(function() {
+
+    // (TEST 22) Test assigning non-default descriptor values on the FontFace.
+    var descriptorTests = Promise.resolve();
+    Object.keys(nonDefaultValues).forEach(function(aDesc) {
+      descriptorTests = descriptorTests.then(function() {
+        var face = new FontFace("test", fontData);
+        return face.loaded.then(function() {
+          var ok_todo = aDesc == "variant" ? todo : ok;
+          face[aDesc] = nonDefaultValues[aDesc][0];
+          ok_todo(face[aDesc] == nonDefaultValues[aDesc][1], "assigned valid non-default value to FontFace." + aDesc + " (TEST 22) ");
+        }, function(aError) {
+          ok(false, "FontFace should have loaded succesfully (TEST 22)");
+        });
+      });
+    });
+    return descriptorTests;
+
+  }).then(function() {
+
+    // (TEST 23) Test assigning invalid descriptor values on the FontFace.
+    var descriptorTests = Promise.resolve();
+    Object.keys(invalidValues).forEach(function(aDesc) {
+      descriptorTests = descriptorTests.then(function() {
+        var face = new FontFace("test", fontData);
+        return face.loaded.then(function() {
+          var ok_todo = aDesc == "variant" ? todo : ok;
+          var exceptionName = "";
+          try {
+            face[aDesc] = invalidValues[aDesc];
+          } catch (ex) {
+            exceptionName = ex.name;
+          }
+          ok_todo(exceptionName == "SyntaxError", "assigning invalid value to FontFace." + aDesc + " should throw a SyntaxError (TEST 23)");
+        }, function(aError) {
+          ok(false, "FontFace should have loaded succesfully (TEST 23)");
+        });
+      });
+    });
+    return descriptorTests;
+
+  }).then(function() {
+
+    // (TEST 24) Test that the status of a FontFace with a non-existing url()
+    // source is set to "loading" right after load() is called, that its .loaded
+    // Promise is returned, and that the Promise is eventually rejected with a
+    // NetworkError and its status is set to "error".
+    var face = new FontFace("test", "url(x)");
+    var result = face.load();
+    is(face.status, "loading", "FontFace.status should be \"loading\" right after load() is called (TEST 24)");
+    is(result, face.loaded, "FontFace.load() should return the .loaded Promise (TEST 24)");
+
+    return result.then(function() {
+      ok(false, "FontFace with a non-existing url() source should not load (TEST 24)");
+    }, function(aError) {
+      is(aError.name, "NetworkError", "FontFace with a non-existing url() source should result in its .loaded Promise being rejected with a NetworkError (TEST 24)");
+      is(face.status, "error", "FontFace with a non-existing url() source should result in its .status being set to \"error\" (TEST 24)");
+    });
+
+  }).then(function() {
+
+    // (TEST 25) Test simple manipulation of the FontFaceSet.
+    var face, face2, all;
+    face = new FontFace("test", "url(x)");
+    face2 = new FontFace("test2", "url(x)");
+    ok(!document.fonts.has(face), "newly created FontFace should not be in document.fonts (TEST 25)");
+    document.fonts.add(face);
+    ok(document.fonts.has(face), "should be able to add a FontFace to document.fonts (TEST 25)");
+    document.fonts.add(face);
+    ok(document.fonts.has(face), "should be able to repeatedly add a FontFace to document.fonts (TEST 25)");
+    ok(document.fonts.delete(face), "FontFaceSet.delete should return true when it succeeds (TEST 25)");
+    ok(!document.fonts.has(face), "FontFace should be gone from document.fonts after delete is called (TEST 25)");
+    ok(!document.fonts.delete(face), "FontFaceSet.delete should return false when it fails (TEST 25)");
+    document.fonts.add(face);
+    document.fonts.add(face2);
+    ok(document.fonts.has(face2), "should be able to add a second FontFace to document.fonts (TEST 25)");
+    document.fonts.clear();
+    ok(!document.fonts.has(face) && !document.fonts.has(face2), "FontFaces should be gone from document.fonts after clear is called (TEST 25)");
+    document.fonts.add(face);
+    document.fonts.add(face2);
+    all = Array.from(document.fonts);
+    is(all[0], face, "FontFaces should be returned in the same order as insertion (TEST 25)");
+    is(all[1], face2, "FontFaces should be returned in the same order as insertion (TEST 25)");
+    document.fonts.add(face);
+    all = Array.from(document.fonts);
+    is(all[0], face, "FontFaces should be not be reordered when a duplicate entry is added (TEST 25)");
+    is(all[1], face2, "FontFaces should be not be reordered when a duplicate entry is added (TEST 25)");
+    document.fonts.clear();
+    return document.fonts.read;
+
+  }).then(function() {
+
+    // (TEST 26) Test that FontFaceSet.ready is replaced, .status is set to
+    // "loading", and a loading event is dispatched when a loading FontFace is
+    // added to it.
+    var awaitEvents = new Promise(function(aResolve, aReject) {
+
+      var onloadingTriggered = false, loadingDispatched = false;
+
+      function check() {
+        if (onloadingTriggered && loadingDispatched) {
+          document.fonts.onloading = null;
+          document.fonts.removeEventListener("loading", listener);
+          aResolve();
+        }
+      }
+
+      var listener = function(aEvent) {
+        is(Object.getPrototypeOf(aEvent), Event.prototype, "loading event should be a plain Event object (TEST 26)");
+        loadingDispatched = true;
+        check();
+      };
+      document.fonts.addEventListener("loading", listener);
+      document.fonts.onloading = function(aEvent) {
+        is(Object.getPrototypeOf(aEvent), Event.prototype, "loading event should be a plain Event object (TEST 26)");
+        onloadingTriggered = true;
+        check();
+      };
+    });
+
+    is(document.fonts.status, "loaded", "FontFaceSet.status initially (TEST 26)");
+
+    var oldReady = document.fonts.ready;
+    var face = new FontFace("test", "url(neverending_font_load.sjs)");
+    face.load();
+    document.fonts.add(face);
+
+    var newReady = document.fonts.ready;
+    isnot(newReady, oldReady, "FontFaceSet.ready should be replaced when a loading FontFace is added to it (TEST 26)");
+    is(document.fonts.status, "loading", "FontFaceSet.status should be set to \"loading\" when a loading FontFace is added to it (TEST 26)");
+
+    return awaitEvents
+        .then(is_pending(newReady, "FontFaceSet.ready should be replaced with a fresh pending Promise when a loading FontFace is added to it", "(TEST 26)"))
+        .then(function() {
+          document.fonts.clear();
+          return document.fonts.ready;
+        });
+
+  }).then(function() {
+
+    // (TEST 27) Test that FontFaceSet.ready is resolved, .status is set to
+    // "loaded", and a loadingdone event (but no loadingerror event) is
+    // dispatched when the only loading FontFace in it is removed.
+    var awaitEvents = new Promise(function(aResolve, aReject) {
+
+      var onloadingdoneTriggered = false, loadingdoneDispatched = false;
+      var onloadingerrorTriggered = false, loadingerrorDispatched = false;
+
+      function check() {
+        document.fonts.onloadingdone = null;
+        document.fonts.onloadingerror = null;
+        document.fonts.removeEventListener("loadingdone", doneListener);
+        document.fonts.removeEventListener("loadingerror", errorListener);
+        aResolve();
+      }
+
+      var doneListener = function(aEvent) {
+        is(Object.getPrototypeOf(aEvent), CSSFontFaceLoadEvent.prototype, "loadingdone event should be a CSSFontFaceLoadEvent object (TEST 27)");
+        is(aEvent.fontfaces.length, 0, "the CSSFontFaceLoadEvent should have an empty fontfaces array (TEST 27)");
+        loadingdoneDispatched = true;
+        check();
+      };
+      document.fonts.addEventListener("loadingdone", doneListener);
+      document.fonts.onloadingdone = function(aEvent) {
+        is(Object.getPrototypeOf(aEvent), CSSFontFaceLoadEvent.prototype, "loadingdone event should be a CSSFontFaceLoadEvent object (TEST 27)");
+        is(aEvent.fontfaces.length, 0, "the CSSFontFaceLoadEvent should have an empty fontfaces array (TEST 27)");
+        onloadingdoneTriggered = true;
+        check();
+      };
+      var errorListener = function(aEvent) {
+        loadingerrorDispatched = true;
+        check();
+      }
+      document.fonts.addEventListener("loadingerror", errorListener);
+      document.fonts.onloadingerror = function(aEvent) {
+        onloadingdoneTriggered = true;
+        check();
+      };
+    });
+
+    is(document.fonts.status, "loaded", "FontFaceSet.status should be \"loaded\" initially (TEST 27)");
+
+    var f = new FontFace("test", "url(neverending_font_load.sjs)");
+    f.load();
+    document.fonts.add(f);
+
+    is(document.fonts.status, "loading", "FontFaceSet.status should be \"loading\" when a loading FontFace is in it (TEST 27)");
+
+    document.fonts.clear();
+
+    return awaitEvents
+        .then(is_resolved_with(document.fonts.ready, document.fonts, "FontFaceSet.ready when the FontFaceSet is cleared", "(TEST 27)"))
+        .then(function() {
+          is(document.fonts.status, "loaded", "FontFaceSet.status should be set to \"loaded\" when it is cleared (TEST 27)");
+          return document.fonts.ready;
+        });
+
+  }).then(function() {
+
+    // (TEST 28) Test that FontFaceSet.ready is replaced, .status is set to
+    // "loading", and a loading event is dispatched when a FontFace in it
+    // starts loading.
+    var awaitEvents = new Promise(function(aResolve, aReject) {
+
+      var onloadingTriggered = false, loadingDispatched = false;
+
+      function check() {
+        if (onloadingTriggered && loadingDispatched) {
+          document.fonts.onloading = null;
+          document.fonts.removeEventListener("loading", listener);
+          aResolve();
+        }
+      }
+
+      var listener = function(aEvent) {
+        is(Object.getPrototypeOf(aEvent), Event.prototype, "loading event should be a plain Event object (TEST 28)");
+        loadingDispatched = true;
+        check();
+      };
+      document.fonts.addEventListener("loading", listener);
+      document.fonts.onloading = function(aEvent) {
+        is(Object.getPrototypeOf(aEvent), Event.prototype, "loading event should be a plain Event object (TEST 28)");
+        onloadingTriggered = true;
+        check();
+      };
+    });
+
+    var oldReady = document.fonts.ready;
+    var face = new FontFace("test", "url(neverending_font_load.sjs)");
+    document.fonts.add(face);
+    face.load();
+
+    var newReady = document.fonts.ready;
+    isnot(newReady, oldReady, "FontFaceSet.ready should be replaced when its only FontFace starts loading (TEST 28)");
+    is(document.fonts.status, "loading", "FontFaceSet.status should be set to \"loading\" when its only FontFace starts loading (TEST 28)");
+
+    return awaitEvents
+        .then(is_pending(newReady, "FontFaceSet.ready when the FontFaceSet's only FontFace starts loading", "(TEST 28)"))
+        .then(function() {
+          document.fonts.clear();
+          return document.fonts.ready;
+        });
+
+  }).then(function() {
+
+    // (TEST 29) Test that a loadingdone and a loadingerror event is dispatched
+    // when a FontFace with status "error" is added to the FontFaceSet.
+    var face;
+    var awaitEvents = new Promise(function(aResolve, aReject) {
+
+      var onloadingdoneTriggered = false, loadingdoneDispatched = false;
+      var onloadingerrorTriggered = false, loadingerrorDispatched = false;
+
+      function check() {
+        if (onloadingdoneTriggered && loadingdoneDispatched &&
+            onloadingerrorTriggered && loadingerrorDispatched) {
+          document.fonts.onloadingdone = null;
+          document.fonts.onloadingerror = null;
+          document.fonts.removeEventListener("loadingdone", doneListener);
+          document.fonts.removeEventListener("loadingerror", errorListener);
+          aResolve();
+        }
+      }
+
+      var doneListener = function(aEvent) {
+        loadingdoneDispatched = true;
+        check();
+      };
+      document.fonts.addEventListener("loadingdone", doneListener);
+      document.fonts.onloadingdone = function(aEvent) {
+        is(Object.getPrototypeOf(aEvent), CSSFontFaceLoadEvent.prototype, "loadingdone event should be a CSSFontFaceLoadEvent object (TEST 29)");
+        is(aEvent.fontfaces.length, 0, "the CSSFontFaceLoadEvent should have an empty fontfaces array (TEST 29)");
+        onloadingdoneTriggered = true;
+        check();
+      };
+      var errorListener = function(aEvent) {
+        is(Object.getPrototypeOf(aEvent), CSSFontFaceLoadEvent.prototype, "loadingerror event should be a CSSFontFaceLoadEvent object (TEST 29)");
+        is(aEvent.fontfaces[0], face, "the CSSFontFaceLoadEvent should have a fontfaces array with the FontFace in it (TEST 29)");
+        loadingerrorDispatched = true;
+        check();
+      }
+      document.fonts.addEventListener("loadingerror", errorListener);
+      document.fonts.onloadingerror = function(aEvent) {
+        onloadingerrorTriggered = true;
+        check();
+      };
+    });
+
+    face = new FontFace("test", new ArrayBuffer(0));
+
+    return face.loaded
+      .then(function() {
+        ok(false, "the FontFace should not load (TEST 29)");
+      }, function(aError) {
+        is(face.status, "error", "FontFace should have status \"error\" (TEST 29)");
+        document.fonts.add(face);
+      })
+      .then(awaitEvents)
+      .then(function() {
+        document.fonts.clear();
+        return document.fonts.ready;
+      });
+
+  }).then(function() {
+
+    // (TEST 30) Test that a loadingdone event is dispatched when a FontFace
+    // with status "loaded" is added to the FontFaceSet.
+    var face;
+    var awaitEvents = new Promise(function(aResolve, aReject) {
+
+      var onloadingdoneTriggered = false, loadingdoneDispatched = false;
+
+      function check() {
+        if (onloadingdoneTriggered && loadingdoneDispatched) {
+          document.fonts.onloadingdone = null;
+          document.fonts.removeEventListener("loadingdone", doneListener);
+          aResolve();
+        }
+      }
+
+      var doneListener = function(aEvent) {
+        loadingdoneDispatched = true;
+        check();
+      };
+      document.fonts.addEventListener("loadingdone", doneListener);
+      document.fonts.onloadingdone = function(aEvent) {
+        is(aEvent.fontfaces[0], face, "the CSSFontFaceLoadEvent should have a fontfaces array with the FontFace in it (TEST 30)");
+        onloadingdoneTriggered = true;
+        check();
+      };
+    });
+
+    face = new FontFace("test", fontData);
+
+    return face.loaded
+      .then(function() {
+        is(face.status, "loaded", "FontFace should have status \"loaded\" (TEST 30)");
+        document.fonts.add(face);
+      })
+      .then(awaitEvents)
+      .then(function() {
+        document.fonts.clear();
+      });
+
+  }).then(function() {
+
+    // (TEST 31) Test that a loadingdone event is dispatched when a FontFace
+    // with status "unloaded" is added to the FontFaceSet and load() is called
+    // on it.
+    var face;
+    var awaitEvents = new Promise(function(aResolve, aReject) {
+
+      var onloadingdoneTriggered = false, loadingdoneDispatched = false;
+
+      function check() {
+        if (loadingdoneDispatched && loadingdoneDispatched) {
+          document.fonts.onloadingdone = null;
+          document.fonts.removeEventListener("loadingdone", doneListener);
+          aResolve();
+        }
+      }
+
+      var doneListener = function(aEvent) {
+        loadingdoneDispatched = true;
+        check();
+      };
+      document.fonts.addEventListener("loadingdone", doneListener);
+      document.fonts.onloadingdone = function(aEvent) {
+        is(aEvent.fontfaces[0], face, "the CSSFontFaceLoadEvent should have a fontfaces array with the FontFace in it (TEST 31)");
+        onloadingdoneTriggered = true;
+        check();
+      };
+    });
+
+    face = new FontFace("test", "url(BitPattern.woff)");
+
+    return face.load()
+        .then(function() {
+          is(face.status, "loaded", "FontFace should have status \"loaded\" (TEST 31)");
+          document.fonts.add(face);
+        })
+        .then(awaitEvents)
+        .then(function() {
+          document.fonts.clear();
+          return document.fonts.ready;
+        });
+  }).then(function() {
+
+    // (TEST 32) Test that pending restyles prevent document.fonts.status
+    // from becoming loaded.
+    var face = new FontFace("test", "url(neverending_font_load.sjs)");
+    face.load();
+    document.fonts.add(face);
+
+    is(document.fonts.status, "loading", "FontFaceSet.status after adding a loading FontFace (TEST 32)");
+
+    document.fonts.clear();
+
+    is(document.fonts.status, "loaded", "FontFaceSet.status after clearing (TEST 32)");
+
+    document.fonts.add(face);
+
+    is(document.fonts.status, "loading", "FontFaceSet.status after adding a loading FontFace again (TEST 32)");
+
+    var div = document.querySelector("div");
+    div.style.color = "blue";
+
+    document.fonts.clear();
+    is(document.fonts.status, "loading", "FontFaceSet.status after clearing but when there is a pending restyle (TEST 32)");
+
+    return awaitRefresh()  // wait for a refresh driver tick
+        .then(function() {
+          is(document.fonts.status, "loaded", "FontFaceSet.status after clearing and the restyle has been flushed (TEST 32)");
+          return document.fonts.ready;
+        });
+
+  }).then(function() {
+
+    // (TEST 33) Test that CSS-connected FontFace objects are created
+    // for @font-face rules in the document.
+    var style = document.querySelector("style");
+    var ruleText = "@font-face { font-family: something; src: url(x); ";
+    Object.keys(nonDefaultValues).forEach(function(aDesc) {
+      ruleText += descriptorNames[aDesc] + ": " + nonDefaultValues[aDesc][0] + "; ";
+    });
+    ruleText += "}";
+
+    style.textContent = ruleText;
+    document.body.offsetTop;
+
+    var rule = style.sheet.cssRules[0];
+
+    var all = Array.from(document.fonts);
+    is(all.length, 1, "document.fonts should contain one FontFace (TEST 34)");
+
+    var face = all[0];
+    is(face.family, "\"something\"", "FontFace should have correct family value (TEST 34)");
+    Object.keys(nonDefaultValues).forEach(function(aDesc) {
+      var ok_todo = aDesc == "variant" ? todo : ok;
+      ok_todo(face[aDesc] == nonDefaultValues[aDesc][1], "FontFace should have correct " + aDesc + " value (TEST 34)");
+    });
+
+    document.fonts.clear();
+    ok(document.fonts.has(face), "CSS-connected FontFace should not be removed from document.fonts when clear is called (TEST 34)");
+
+    var exceptionName = "";
+    try {
+      document.fonts.delete(face);
+    } catch (ex) {
+      exceptionName = ex.name;
+    }
+    ok(exceptionName == "InvalidModificationError", "attempting to remove CSS-connected FontFace from document.fonts should throw an InvalidModificationError (TEST 34)");
+    ok(document.fonts.has(face), "CSS-connected FontFace should not be removed from document.fonts when delete is called (TEST 34)");
+
+    style.textContent = "";
+    document.body.offsetTop;
+
+    ok(!document.fonts.has(face), "CSS-connected FontFace should be removed from document.fonts once the rule has been removed (TEST 34)");
+
+    document.fonts.add(face);
+    ok(document.fonts.has(face), "previously CSS-connected FontFace should be able to be added to document.fonts (TEST 34)");
+
+    document.fonts.delete(face);
+    ok(!document.fonts.has(face), "previously CSS-connected FontFace should be able to be removed from document.fonts (TEST 34)");
+
+  }).then(function() {
+
+    // (TEST 34) Test that a pending style sheet load prevents
+    // document.fonts.status from being set to "loaded".
+
+    // First, add a FontFace to document.fonts that will load soon.
+    var face = new FontFace("test", "url(BitPattern.woff)");
+    face.load();
+    document.fonts.add(face);
+
+    // Next, add a style sheet reference.
+    var link = document.createElement("link");
+    link.rel = "stylesheet";
+    link.href = "neverending_stylesheet_load.sjs";
+    link.type = "text/css";
+    document.head.appendChild(link);
+
+    return setTimeoutZero()  // wait for the style sheet to start loading
+        .then(function() {
+          document.fonts.clear();
+          is(document.fonts.status, "loading", "FontFaceSet.status when the FontFaceSet has been cleared of loading FontFaces but there is a pending style sheet load (TEST 33)");
+          document.head.removeChild(link);
+          // XXX Removing the <link> element won't cancel the load of the
+          // style sheet, so we can't do that to test that
+          // document.fonts.ready is resolved once there are no more
+          // loading style sheets.
+        });
+
+    // NOTE: It is important that this style sheet test comes last in the file,
+    // as the neverending style sheet load will interfere with subsequent
+    // sub-tests.
+
+  }).then(function() {
+
+    // End of the tests.
+    SimpleTest.finish();
+
+  }, function(aError) {
+
+    // Something failed.
+    ok(false, "Something failed: " + aError);
+    SimpleTest.finish();
+
+  });
+}
+
+if (SpecialPowers.getBoolPref("layout.css.font-loading-api.enabled")) {
+  runTest();
+} else {
+  ok(true, "CSS Font Loading API is not enabled.");
+}
+</script>
+
+<style></style>
+<div></div>