layout/style/test/test_font_loading_api.html
author Cameron McCormack <cam@mcc.id.au>
Fri, 27 Mar 2015 21:13:21 +1100
changeset 264946 1df07e80e3c7a8a127b95d0f98b71a4af77300df
parent 264945 346bbcaeff387faadd2cc493f1a6fd77b2fcea14
child 264953 b1f9fa1917187b8303f7cef6a27ab6aa1496372f
permissions -rw-r--r--
Bug 1145506 - Make FontFace constructor fail on invalid src strings but otherwise create user font entries immediately. r=jdaggett

<!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+0-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
// different 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+300-3FF"],
  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", "a 1"
];

// 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.  Because it has an implicit initial
    // load() call, it should either be "loading" if the browser is
    // asynchronously parsing the font data, or "error" if it parsed
    // it immediately.
    var status = new FontFace("test", new ArrayBuffer(0)).status;
    ok(status == "loading" || status == "error", "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.  Because it has an implicit initial load() call, it
    // should either be "loading" if the browser is asynchronously parsing the
    // font data, or "loaded" if it parsed it immediately.
    status = new FontFace("test", fontData).status;
    ok(status == "loading" || status == "loaded", "initial value of FontFace.status when a valid ArrayBuffer source is used (TEST 8)");

    // (TEST 9) (old test became redundant with TEST 19)

  }).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) (old test became redundant with TEST 21)

  }).then(function() {

    // (TEST 12) (old test became redundant with TEST 20)

  }).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)");
        is(face.family, "", "FontFace.family should be the empty string 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);
        is(face.status, "error", "FontFace.status should be \"error\" when constructed with an invalid url() src " + aSrc + " (TEST 19");
        return face.loaded.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 constructed 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(aFace) {
      is(face.status, "loaded", "status of FontFace constructed with a valid ArrayBuffer source should eventually be \"loaded\" (TEST 20)");
      is(face, aFace, "FontFace.loaded was resolved with the FontFace object once 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.ready;

  }).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(function() {
          return 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(function() {
          return 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(function() {
          return 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 that eventually becomes 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", "url(x)");
    face.load();
    is(face.status, "loading", "FontFace should have status \"loading\" (TEST 29)");
    document.fonts.add(face);

    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)");
        return awaitEvents;
      })
      .then(function() {
        document.fonts.clear();
        return document.fonts.ready;
      });

  }).then(function() {

    // (TEST 30) Test that a loadingdone event is dispatched when a FontFace
    // that eventually becomes 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", "url(BitPattern.woff?test30)");
    face.load();
    is(face.status, "loading", "FontFace should have status \"loading\" (TEST 30)");
    document.fonts.add(face);

    return face.loaded
      .then(function() {
        is(face.status, "loaded", "FontFace should have status \"loaded\" (TEST 30)");
        return 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 (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 31)");
        onloadingdoneTriggered = true;
        check();
      };
    });

    face = new FontFace("test", "url(BitPattern.woff?test31)");
    is(face.status, "unloaded", "FontFace should have status \"unloaded\" (TEST 31)");
    document.fonts.add(face);

    return face.load()
        .then(function() {
          return awaitEvents;
        })
        .then(function() {
          is(face.status, "loaded", "FontFace should have status \"loaded\" (TEST 31)");
          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.

    is(document.fonts.status, "loaded", "document.fonts.status should initially be loaded (TEST 33)");

    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;

    var rule = style.sheet.cssRules[0];

    var all = Array.from(document.fonts);
    is(all.length, 1, "document.fonts should contain one FontFace (TEST 33)");

    var face = all[0];
    is(face.family, "\"something\"", "FontFace should have correct family value (TEST 33)");
    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 33)");
    });

    is(document.fonts.status, "loaded", "document.fonts.status should still be loaded (TEST 33)");
    is(face.status, "unloaded", "FontFace.status should be unloaded (TEST 33)");

    document.fonts.clear();
    ok(document.fonts.has(face), "CSS-connected FontFace should not be removed from document.fonts when clear is called (TEST 33)");

    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 33)");
    ok(document.fonts.has(face), "CSS-connected FontFace should not be removed from document.fonts when delete is called (TEST 33)");

    style.textContent = "";

    ok(!document.fonts.has(face), "CSS-connected FontFace should be removed from document.fonts once the rule has been removed (TEST 33)");

    is(document.fonts.status, "loaded", "document.fonts.status should still be loaded after rule is removed (TEST 33)");
    is(face.status, "unloaded", "FontFace.status should still be unloaded after rule is removed (TEST 33)");

    document.fonts.add(face);
    ok(document.fonts.has(face), "previously CSS-connected FontFace should be able to be added to document.fonts (TEST 33)");

    is(document.fonts.status, "loaded", "document.fonts.status should still be loaded after now disconnected FontFace is added (TEST 33)");
    is(face.status, "unloaded", "FontFace.status should still be unloaded after now disconnected FontFace is added (TEST 33)");

    document.fonts.delete(face);
    ok(!document.fonts.has(face), "previously CSS-connected FontFace should be able to be removed from document.fonts (TEST 33)");

  }).then(function() {

    // (TEST 34) Test that descriptor getters for unspecified descriptors on
    // CSS-connected FontFace objects return their default values.
    var style = document.querySelector("style");
    var ruleText = "@font-face { font-family: something; src: url(x); }";

    style.textContent = ruleText;

    var all = Array.from(document.fonts);
    var face = all[0];

    Object.keys(defaultValues).forEach(function(aDesc) {
      is(face[aDesc], defaultValues[aDesc], "FontFace should return default value for " + aDesc + " (TEST 34)");
    });

    style.textContent = "";

  }).then(function() {

    // (TEST 35) Test that no loadingdone event is dispatched when a FontFace
    // with "loaded" status is added to a "loaded" FontFaceSet.
    var gotLoadingDone = false;
    document.fonts.onloadingdone = function(aEvent) {
      gotLoadingDone = true;
    };

    is(document.fonts.status, "loaded", "document.fonts.status should have status \"loaded\" (TEST 35)");
    var face = new FontFace("test", fontData);

    return face.loaded
      .then(function() {
        is(face.status, "loaded", "FontFace should have status \"loaded\" (TEST 35)");
        document.fonts.add(face);
        is(document.fonts.status, "loaded", "document.fonts.status should still have status \"loaded\" (TEST 35)");
        return document.fonts.ready;
      })
      .then(function() {
        ok(!gotLoadingDone, "loadingdone event should not be dispatched (TEST 35)");
        document.fonts.onloadingdone = null;
        document.fonts.clear();
      });

  }).then(function() {

    // (TEST 36) Test that no loadingdone or loadingerror event is dispatched
    // when a FontFace with "error" status is added to a "loaded" FontFaceSet.
    var gotLoadingDone = false, gotLoadingError = false;
    document.fonts.onloadingdone = function(aEvent) {
      gotLoadingDone = true;
    };
    document.fonts.onloadingerror = function(aEvent) {
      gotLoadingError = true;
    };

    is(document.fonts.status, "loaded", "document.fonts.status should have status \"loaded\" (TEST 36)");
    var face = new FontFace("test", new ArrayBuffer(0));

    return face.loaded
      .then(function() {
        ok(false, "FontFace should not have loaded (TEST 36)");
      }, function() {
        is(face.status, "error", "FontFace should have status \"error\" (TEST 36)");
        document.fonts.add(face);
        is(document.fonts.status, "loaded", "document.fonts.status should still have status \"loaded\" (TEST 36)");
        return document.fonts.ready;
      })
      .then(function() {
        ok(!gotLoadingDone, "loadingdone event should not be dispatched (TEST 36)");
        ok(!gotLoadingError, "loadingerror event should not be dispatched (TEST 36)");
        document.fonts.onloadingdone = null;
        document.fonts.onloadingerror = null;
        document.fonts.clear();
      });

  }).then(function() {

    // (TEST 37) Test that a FontFace only has one loadingdone event dispatched
    // at the FontFaceSet containing it.

    var events = [], face, face2;

    document.fonts.onloadingdone = function(e) {
      events.push(e);
    };
    document.fonts.onloadingerror = function(e) {
      events.push(e);
    };

    is(document.fonts.status, "loaded", "document.fonts.status should have status \"loaded\" (TEST 37)");

    face = new FontFace("test", "url(BitPattern.woff?test37a)");
    face.load();
    document.fonts.add(face);
    is(document.fonts.status, "loading", "document.fonts.status should have status \"loading\" after first font added (TEST 37)");

    return document.fonts.ready
      .then(function() {
        is(document.fonts.status, "loaded", "document.fonts.status should have status \"loaded\" after first font loaded (TEST 37)");
        is(face.status, "loaded", "first FontFace should have status \"loaded\" (TEST 37)");

        face2 = new FontFace("test2", "url(BitPattern.woff?test37b)");
        face2.load();
        document.fonts.add(face2);
        is(document.fonts.status, "loading", "document.fonts.status should have status \"loading\" after second font added (TEST 37)");

        return document.fonts.ready;
      }).then(function() {
        is(document.fonts.status, "loaded", "document.fonts.status should have status \"loaded\" after second font loaded (TEST 37)");
        is(face2.status, "loaded", "second FontFace should have status \"loaded\" (TEST 37)");

        is(events.length, 2, "should receive two events (TEST 37)");

        is(events[0].type, "loadingdone", "first event should be \"loadingdone\" (TEST 37)");
        is(events[0].fontfaces.length, 1, "first event should have 1 FontFace (TEST 37)");
        is(events[0].fontfaces[0], face, "first event should have the first FontFace");

        is(events[1].type, "loadingdone", "second event should be \"loadingdone\" (TEST 37)");
        is(events[1].fontfaces.length, 1, "second event should only have 1 FontFace (TEST 37)");
        is(events[1].fontfaces[0], face2, "second event should have the second FontFace (TEST 37)");

        document.fonts.onloadingdone = null;
        document.fonts.onloadingerror = null;
        document.fonts.clear();
        return document.fonts.ready;
      });

  }).then(function() {

    // (TEST 38) Test that a FontFace only has one loadingerror event dispatched
    // at the FontFaceSet containing it.

    var events = [], face, face2;

    document.fonts.onloadingdone = function(e) {
      events.push(e);
    };
    document.fonts.onloadingerror = function(e) {
      events.push(e);
    };

    is(document.fonts.status, "loaded", "document.fonts.status should have status \"loaded\" (TEST 38)");

    face = new FontFace("test", "url(x)");
    face.load();
    document.fonts.add(face);
    is(document.fonts.status, "loading", "document.fonts.status should have status \"loading\" after first font added (TEST 38)");

    return document.fonts.ready
      .then(function() {
        is(document.fonts.status, "loaded", "document.fonts.status should have status \"loaded\" after first font failed to load (TEST 38)");
        is(face.status, "error", "first FontFace should have status \"error\" (TEST 38)");

        face2 = new FontFace("test2", "url(x)");
        face2.load();
        document.fonts.add(face2);
        is(document.fonts.status, "loading", "document.fonts.status should have status \"loading\" after second font added (TEST 38)");

        return document.fonts.ready;
      }).then(function() {
        is(document.fonts.status, "loaded", "document.fonts.status should have status \"loaded\" after second font failed to load (TEST 38)");
        is(face2.status, "error", "second FontFace should have status \"error\" (TEST 38)");

        is(events.length, 4, "should receive four events (TEST 38)");

        is(events[0].type, "loadingdone", "first event should be \"loadingdone\" (TEST 38)");
        is(events[0].fontfaces.length, 0, "first event should have no FontFaces (TEST 38)");

        is(events[1].type, "loadingerror", "second event should be \"loadingerror\" (TEST 38)");
        is(events[1].fontfaces.length, 1, "second event should have 1 FontFace (TEST 38)");
        is(events[1].fontfaces[0], face, "second event should have the first FontFace");

        is(events[2].type, "loadingdone", "third event should be \"loadingdone\" (TEST 38)");
        is(events[2].fontfaces.length, 0, "third event should have no FontFaces (TEST 38)");

        is(events[3].type, "loadingerror", "third event should be \"loadingerror\" (TEST 38)");
        is(events[3].fontfaces.length, 1, "third event should only have 1 FontFace (TEST 38)");
        is(events[3].fontfaces[0], face2, "third event should have the second FontFace");

        document.fonts.onloadingdone = null;
        document.fonts.onloadingerror = null;
        document.fonts.clear();
        return document.fonts.ready;
      });

  }).then(function() {

    // (TEST 39) Test that a FontFace for an @font-face rule only has one
    // loadingdone event dispatched at the FontFaceSet containing it.

    var style = document.querySelector("style");
    var ruleText = "@font-face { font-family: test; src: url(BitPattern.woff?test39a); } " +
                   "@font-face { font-family: test2; src: url(BitPattern.woff?test39b); }";

    style.textContent = ruleText;

    var all = Array.from(document.fonts);
    var events = [];

    document.fonts.onloadingdone = function(e) {
      events.push(e);
    };
    document.fonts.onloadingerror = function(e) {
      events.push(e);
    };

    is(document.fonts.status, "loaded", "document.fonts.status should have status \"loaded\" (TEST 39)");

    all[0].load();
    is(document.fonts.status, "loading", "document.fonts.status should have status \"loading\" after first font loading (TEST 39)");

    return document.fonts.ready
      .then(function() {
        is(document.fonts.status, "loaded", "document.fonts.status should have status \"loaded\" after first font loaded (TEST 39)");
        is(all[0].status, "loaded", "first FontFace should have status \"loaded\" (TEST 39)");
        is(all[1].status, "unloaded", "second FontFace should have status \"unloaded\" (TEST 39)");

        all[1].load();
        is(document.fonts.status, "loading", "document.fonts.status should have status \"loading\" after second font loading (TEST 39)");

        return document.fonts.ready;
      }).then(function() {
        is(document.fonts.status, "loaded", "document.fonts.status should have status \"loaded\" after second font loaded (TEST 39)");
        is(all[1].status, "loaded", "second FontFace should have status \"loaded\" (TEST 39)");

        is(events.length, 2, "should receive two events (TEST 39)");

        is(events[0].type, "loadingdone", "first event should be \"loadingdone\" (TEST 39)");
        is(events[0].fontfaces.length, 1, "first event should have 1 FontFace (TEST 39)");
        is(events[0].fontfaces[0], all[0], "first event should have the first FontFace");

        is(events[1].type, "loadingdone", "second event should be \"loadingdone\" (TEST 39)");
        is(events[1].fontfaces.length, 1, "second event should only have 1 FontFace (TEST 39)");
        is(events[1].fontfaces[0], all[1], "second event should have the second FontFace (TEST 39)");

        document.fonts.onloadingdone = null;
        document.fonts.onloadingerror = null;
        document.fonts.clear();
        return document.fonts.ready;
      });

  }).then(function() {

    // (TEST LAST) 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?testlast)");
    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 LAST)");
          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>