tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

animation_utils.js (27129B)


      1 //----------------------------------------------------------------------
      2 //
      3 // Common testing functions
      4 //
      5 //----------------------------------------------------------------------
      6 
      7 /* eslint-disable mozilla/no-comparison-or-assignment-inside-ok */
      8 
      9 function advance_clock(milliseconds) {
     10  SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(milliseconds);
     11 }
     12 
     13 // Test-element creation/destruction and event checking
     14 (function () {
     15  var gElem;
     16  var gEventsReceived = [];
     17 
     18  function new_div(style) {
     19    return new_element("div", style);
     20  }
     21 
     22  // Creates a new |tagname| element with inline style |style| and appends
     23  // it as a child of the element with ID 'display'.
     24  // The element will also be given the class 'target' which can be used
     25  // for additional styling.
     26  function new_element(tagname, style) {
     27    if (gElem) {
     28      ok(false, "test author forgot to call done_div/done_elem");
     29    }
     30    if (typeof style != "string") {
     31      ok(false, "test author forgot to pass argument");
     32    }
     33    if (!document.getElementById("display")) {
     34      ok(false, "no 'display' element to append to");
     35    }
     36    gElem = document.createElement(tagname);
     37    gElem.setAttribute("style", style);
     38    gElem.classList.add("target");
     39    document.getElementById("display").appendChild(gElem);
     40    return [gElem, getComputedStyle(gElem, "")];
     41  }
     42 
     43  function listen() {
     44    if (!gElem) {
     45      ok(false, "test author forgot to call new_div before listen");
     46    }
     47    gEventsReceived = [];
     48    function listener(event) {
     49      gEventsReceived.push(event);
     50    }
     51    gElem.addEventListener("animationstart", listener);
     52    gElem.addEventListener("animationiteration", listener);
     53    gElem.addEventListener("animationend", listener);
     54  }
     55 
     56  function check_events(eventsExpected, desc) {
     57    // This function checks that the list of eventsExpected matches
     58    // the received events -- but it only checks the properties that
     59    // are present on eventsExpected.
     60    is(
     61      gEventsReceived.length,
     62      eventsExpected.length,
     63      "number of events received for " + desc
     64    );
     65    for (
     66      var i = 0,
     67        i_end = Math.min(eventsExpected.length, gEventsReceived.length);
     68      i != i_end;
     69      ++i
     70    ) {
     71      var exp = eventsExpected[i];
     72      var rec = gEventsReceived[i];
     73      for (var prop in exp) {
     74        if (prop == "elapsedTime") {
     75          // Allow floating point error.
     76          ok(
     77            Math.abs(rec.elapsedTime - exp.elapsedTime) < 0.000002,
     78            "events[" +
     79              i +
     80              "]." +
     81              prop +
     82              " for " +
     83              desc +
     84              " received=" +
     85              rec.elapsedTime +
     86              " expected=" +
     87              exp.elapsedTime
     88          );
     89        } else {
     90          is(
     91            rec[prop],
     92            exp[prop],
     93            "events[" + i + "]." + prop + " for " + desc
     94          );
     95        }
     96      }
     97    }
     98    for (var i = eventsExpected.length; i < gEventsReceived.length; ++i) {
     99      ok(false, "unexpected " + gEventsReceived[i].type + " event for " + desc);
    100    }
    101    gEventsReceived = [];
    102  }
    103 
    104  function done_element() {
    105    if (!gElem) {
    106      ok(
    107        false,
    108        "test author called done_element/done_div without matching" +
    109          " call to new_element/new_div"
    110      );
    111    }
    112    gElem.remove();
    113    gElem = null;
    114    if (gEventsReceived.length) {
    115      ok(false, "caller should have called check_events");
    116    }
    117  }
    118 
    119  [new_div, new_element, listen, check_events, done_element].forEach(
    120    function (fn) {
    121      window[fn.name] = fn;
    122    }
    123  );
    124  window.done_div = done_element;
    125 })();
    126 
    127 function px_to_num(str) {
    128  return Number(String(str).match(/^([\d.]+)px$/)[1]);
    129 }
    130 
    131 function bezier(x1, y1, x2, y2) {
    132  // Cubic bezier with control points (0, 0), (x1, y1), (x2, y2), and (1, 1).
    133  function x_for_t(t) {
    134    var omt = 1 - t;
    135    return 3 * omt * omt * t * x1 + 3 * omt * t * t * x2 + t * t * t;
    136  }
    137  function y_for_t(t) {
    138    var omt = 1 - t;
    139    return 3 * omt * omt * t * y1 + 3 * omt * t * t * y2 + t * t * t;
    140  }
    141  function t_for_x(x) {
    142    // Binary subdivision.
    143    var mint = 0,
    144      maxt = 1;
    145    for (var i = 0; i < 30; ++i) {
    146      var guesst = (mint + maxt) / 2;
    147      var guessx = x_for_t(guesst);
    148      if (x < guessx) {
    149        maxt = guesst;
    150      } else {
    151        mint = guesst;
    152      }
    153    }
    154    return (mint + maxt) / 2;
    155  }
    156  return function bezier_closure(x) {
    157    if (x == 0) {
    158      return 0;
    159    }
    160    if (x == 1) {
    161      return 1;
    162    }
    163    return y_for_t(t_for_x(x));
    164  };
    165 }
    166 
    167 function step_end(nsteps) {
    168  return function step_end_closure(x) {
    169    return Math.floor(x * nsteps) / nsteps;
    170  };
    171 }
    172 
    173 function step_start(nsteps) {
    174  var stepend = step_end(nsteps);
    175  return function step_start_closure(x) {
    176    return 1.0 - stepend(1.0 - x);
    177  };
    178 }
    179 
    180 var gTF = {
    181  ease: bezier(0.25, 0.1, 0.25, 1),
    182  linear: function (x) {
    183    return x;
    184  },
    185  ease_in: bezier(0.42, 0, 1, 1),
    186  ease_out: bezier(0, 0, 0.58, 1),
    187  ease_in_out: bezier(0.42, 0, 0.58, 1),
    188  step_start: step_start(1),
    189  step_end: step_end(1),
    190 };
    191 
    192 function is_approx(float1, float2, error, desc) {
    193  ok(
    194    Math.abs(float1 - float2) < error,
    195    desc + ": " + float1 + " and " + float2 + " should be within " + error
    196  );
    197 }
    198 
    199 function findKeyframesRule(name) {
    200  for (var i = 0; i < document.styleSheets.length; i++) {
    201    var match = [].find.call(document.styleSheets[i].cssRules, function (rule) {
    202      return rule.type == CSSRule.KEYFRAMES_RULE && rule.name == name;
    203    });
    204    if (match) {
    205      return match;
    206    }
    207  }
    208  return undefined;
    209 }
    210 
    211 function isOMTAWorking() {
    212  function waitForDocumentLoad() {
    213    return new Promise(function (resolve, reject) {
    214      if (document.readyState === "complete") {
    215        resolve();
    216      } else {
    217        window.addEventListener("load", resolve);
    218      }
    219    });
    220  }
    221 
    222  function loadPaintListener() {
    223    return new Promise(function (resolve, reject) {
    224      if (typeof window.waitForAllPaints !== "function") {
    225        var script = document.createElement("script");
    226        script.onload = resolve;
    227        script.onerror = function () {
    228          reject(new Error("Failed to load paint listener"));
    229        };
    230        script.src = "/tests/SimpleTest/paint_listener.js";
    231        var firstScript = document.scripts[0];
    232        firstScript.parentNode.insertBefore(script, firstScript);
    233      } else {
    234        resolve();
    235      }
    236    });
    237  }
    238 
    239  // Create keyframes rule
    240  const animationName = "a6ce3091ed85"; // Random name to avoid clashes
    241  var ruleText =
    242    "@keyframes " +
    243    animationName +
    244    " { from { opacity: 0.5 } to { opacity: 0.5 } }";
    245  var style = document.createElement("style");
    246  style.appendChild(document.createTextNode(ruleText));
    247  document.head.appendChild(style);
    248 
    249  // Create animation target
    250  var div = document.createElement("div");
    251  document.body.appendChild(div);
    252 
    253  // Give the target geometry so it is eligible for layerization
    254  div.style.width = "100px";
    255  div.style.height = "100px";
    256  div.style.backgroundColor = "white";
    257 
    258  var utils = SpecialPowers.DOMWindowUtils;
    259 
    260  // Common clean up code
    261  var cleanUp = function () {
    262    div.remove();
    263    style.remove();
    264    if (utils.isTestControllingRefreshes) {
    265      utils.restoreNormalRefresh();
    266    }
    267  };
    268 
    269  return waitForDocumentLoad()
    270    .then(loadPaintListener)
    271    .then(function () {
    272      // Put refresh driver under test control and flush all pending style,
    273      // layout and paint to avoid the situation that waitForPaintsFlush()
    274      // receives unexpected MozAfterpaint event for those pending
    275      // notifications.
    276      utils.advanceTimeAndRefresh(0);
    277      return waitForPaintsFlushed();
    278    })
    279    .then(function () {
    280      div.style.animation = animationName + " 10s";
    281 
    282      return waitForPaintsFlushed();
    283    })
    284    .then(function () {
    285      var opacity = utils.getOMTAStyle(div, "opacity");
    286      cleanUp();
    287      return Promise.resolve(opacity == 0.5);
    288    })
    289    .catch(function (err) {
    290      cleanUp();
    291      return Promise.reject(err);
    292    });
    293 }
    294 
    295 // Checks if off-main thread animation (OMTA) is available, and if it is, runs
    296 // the provided callback function. If OMTA is not available or is not
    297 // functioning correctly, the second callback, aOnSkip, is run instead.
    298 //
    299 // This function also does an internal test to verify that OMTA is working at
    300 // all so that if OMTA is not functioning correctly when it is expected to
    301 // function only a single failure is produced.
    302 //
    303 // Since this function relies on various asynchronous operations, the caller is
    304 // responsible for calling SimpleTest.waitForExplicitFinish() before calling
    305 // this and SimpleTest.finish() within aTestFunction and aOnSkip.
    306 //
    307 // specialPowersForPrefs exists because some SpecialPowers objects apparently
    308 // can get prefs and some can't; callers that would normally have one of the
    309 // latter but can get their hands on one of the former can pass it in
    310 // explicitly.
    311 function runOMTATest(aTestFunction, aOnSkip, specialPowersForPrefs) {
    312  const OMTAPrefKey = "layers.offmainthreadcomposition.async-animations";
    313  var utils = SpecialPowers.DOMWindowUtils;
    314  if (!specialPowersForPrefs) {
    315    specialPowersForPrefs = SpecialPowers;
    316  }
    317  var expectOMTA =
    318    utils.layerManagerRemote &&
    319    // ^ Off-main thread animation cannot be used if off-main
    320    // thread composition (OMTC) is not available
    321    specialPowersForPrefs.getBoolPref(OMTAPrefKey);
    322 
    323  isOMTAWorking()
    324    .then(function (isWorking) {
    325      if (expectOMTA) {
    326        if (isWorking) {
    327          aTestFunction();
    328        } else {
    329          // We only call this when we know it will fail as otherwise in the
    330          // regular success case we will end up inflating the "passed tests"
    331          // count by 1
    332          ok(isWorking, "OMTA should work");
    333          aOnSkip();
    334        }
    335      } else {
    336        todo(
    337          isWorking,
    338          "OMTA should ideally work, though we don't expect it to work on " +
    339            "this platform/configuration"
    340        );
    341        aOnSkip();
    342      }
    343    })
    344    .catch(function (err) {
    345      ok(false, err);
    346      aOnSkip();
    347    });
    348 }
    349 
    350 // Common architecture for setting up a series of asynchronous animation tests
    351 //
    352 // Usage example:
    353 //
    354 //    addAsyncAnimTest(function *() {
    355 //       .. do work ..
    356 //       yield functionThatReturnsAPromise();
    357 //       .. do work ..
    358 //    });
    359 //    runAllAsyncAnimTests().then(SimpleTest.finish());
    360 //
    361 (function () {
    362  var tests = [];
    363 
    364  window.addAsyncAnimTest = function (generator) {
    365    tests.push(generator);
    366  };
    367 
    368  // Returns a promise when all tests have run
    369  window.runAllAsyncAnimTests = function (aOnAbort) {
    370    // runAsyncAnimTest returns a Promise that is resolved when the
    371    // test is finished so we can chain them together
    372    return tests.reduce(function (sequence, test) {
    373      return sequence.then(function () {
    374        return runAsyncAnimTest(test, aOnAbort);
    375      });
    376    }, Promise.resolve() /* the start of the sequence */);
    377  };
    378 
    379  // Takes a generator function that represents a test case. Each point in the
    380  // test case that waits asynchronously for some result yields a Promise that
    381  // is resolved when the asynchronous action has completed. By chaining these
    382  // intermediate results together we run the test to completion.
    383  //
    384  // This method itself returns a Promise that is resolved when the generator
    385  // function has completed.
    386  //
    387  // This arrangement is based on add_task() which is currently only available
    388  // in mochitest-chrome (bug 872229). If add_task becomes available in
    389  // mochitest-plain, we can remove this function and use add_task instead.
    390  function runAsyncAnimTest(aTestFunc, aOnAbort) {
    391    var generator;
    392 
    393    function step(arg) {
    394      var next;
    395      try {
    396        next = generator.next(arg);
    397      } catch (e) {
    398        return Promise.reject(e);
    399      }
    400      if (next.done) {
    401        return Promise.resolve(next.value);
    402      }
    403      return Promise.resolve(next.value).then(step, function (err) {
    404        throw err;
    405      });
    406    }
    407 
    408    // Put refresh driver under test control
    409    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(0);
    410 
    411    // Run test
    412    var promise = aTestFunc();
    413    if (!promise.then) {
    414      generator = promise;
    415      promise = step();
    416    }
    417    return promise
    418      .catch(function (err) {
    419        ok(false, err.message);
    420        if (typeof aOnAbort == "function") {
    421          aOnAbort();
    422        }
    423      })
    424      .then(function () {
    425        // Restore clock
    426        SpecialPowers.DOMWindowUtils.restoreNormalRefresh();
    427      });
    428  }
    429 })();
    430 
    431 //----------------------------------------------------------------------
    432 //
    433 // Helper functions for testing animated values on the compositor
    434 //
    435 //----------------------------------------------------------------------
    436 
    437 const RunningOn = {
    438  MainThread: 0,
    439  Compositor: 1,
    440  Either: 2,
    441  TodoMainThread: 3,
    442  TodoCompositor: 4,
    443 };
    444 
    445 const ExpectComparisonTo = {
    446  Pass: 1,
    447  Fail: 2,
    448 };
    449 
    450 (function () {
    451  window.omta_todo_is = function (
    452    elem,
    453    property,
    454    expected,
    455    runningOn,
    456    desc,
    457    pseudo
    458  ) {
    459    return omta_is_approx(
    460      elem,
    461      property,
    462      expected,
    463      0,
    464      runningOn,
    465      desc,
    466      ExpectComparisonTo.Fail,
    467      pseudo
    468    );
    469  };
    470 
    471  window.omta_is = function (
    472    elem,
    473    property,
    474    expected,
    475    runningOn,
    476    desc,
    477    pseudo
    478  ) {
    479    return omta_is_approx(
    480      elem,
    481      property,
    482      expected,
    483      0,
    484      runningOn,
    485      desc,
    486      ExpectComparisonTo.Pass,
    487      pseudo
    488    );
    489  };
    490 
    491  // Many callers of this method will pass 'undefined' for
    492  // expectedComparisonResult.
    493  window.omta_is_approx = function (
    494    elem,
    495    property,
    496    expected,
    497    tolerance,
    498    runningOn,
    499    desc,
    500    expectedComparisonResult,
    501    pseudo
    502  ) {
    503    // Check input
    504    // FIXME: Auto generate this array.
    505    const omtaProperties = [
    506      "transform",
    507      "translate",
    508      "rotate",
    509      "scale",
    510      "offset-path",
    511      "offset-distance",
    512      "offset-rotate",
    513      "offset-anchor",
    514      "offset-position",
    515      "opacity",
    516      "background-color",
    517    ];
    518    if (!omtaProperties.includes(property)) {
    519      ok(false, property + " is not an OMTA property");
    520      return;
    521    }
    522    var normalize;
    523    var compare;
    524    var normalizedToString = JSON.stringify;
    525    switch (property) {
    526      case "offset-path":
    527      case "offset-distance":
    528      case "offset-rotate":
    529      case "offset-anchor":
    530      case "offset-position":
    531      case "translate":
    532      case "rotate":
    533      case "scale":
    534        if (runningOn == RunningOn.MainThread) {
    535          normalize = value => value;
    536          compare = function (a, b, error) {
    537            return a == b;
    538          };
    539          break;
    540        }
    541      // fall through
    542      case "transform":
    543        normalize = convertTo3dMatrix;
    544        compare = matricesRoughlyEqual;
    545        normalizedToString = convert3dMatrixToString;
    546        break;
    547      case "opacity":
    548        normalize = parseFloat;
    549        compare = function (a, b, error) {
    550          return Math.abs(a - b) <= error;
    551        };
    552        break;
    553      default:
    554        normalize = value => value;
    555        compare = function (a, b, error) {
    556          return a == b;
    557        };
    558        break;
    559    }
    560 
    561    if (!!expected.compositorValue) {
    562      const originalNormalize = normalize;
    563      normalize = value =>
    564        !!value.compositorValue
    565          ? originalNormalize(value.compositorValue)
    566          : originalNormalize(value);
    567    }
    568 
    569    // Get actual values
    570    var compositorStr = SpecialPowers.DOMWindowUtils.getOMTAStyle(
    571      elem,
    572      property,
    573      pseudo
    574    );
    575    var computedStr = window.getComputedStyle(elem, pseudo)[property];
    576 
    577    // Prepare expected value
    578    var expectedValue = normalize(expected);
    579    if (expectedValue === null) {
    580      ok(
    581        false,
    582        desc +
    583          ": test author should provide a valid 'expected' value" +
    584          " - got " +
    585          expected.toString()
    586      );
    587      return;
    588    }
    589 
    590    // Check expected value appears in the right place
    591    var actualStr;
    592    switch (runningOn) {
    593      case RunningOn.Either:
    594        runningOn =
    595          compositorStr !== "" ? RunningOn.Compositor : RunningOn.MainThread;
    596        actualStr = compositorStr !== "" ? compositorStr : computedStr;
    597        break;
    598 
    599      case RunningOn.Compositor:
    600        if (compositorStr === "") {
    601          ok(false, desc + ": should be animating on compositor");
    602          return;
    603        }
    604        actualStr = compositorStr;
    605        break;
    606 
    607      case RunningOn.TodoMainThread:
    608        todo(
    609          compositorStr === "",
    610          desc + ": should NOT be animating on compositor"
    611        );
    612        actualStr = compositorStr === "" ? computedStr : compositorStr;
    613        break;
    614 
    615      case RunningOn.TodoCompositor:
    616        todo(
    617          compositorStr !== "",
    618          desc + ": should be animating on compositor"
    619        );
    620        actualStr = compositorStr !== "" ? computedStr : compositorStr;
    621        break;
    622 
    623      default:
    624        if (compositorStr !== "") {
    625          ok(false, desc + ": should NOT be animating on compositor");
    626          return;
    627        }
    628        actualStr = computedStr;
    629        break;
    630    }
    631 
    632    var okOrTodo =
    633      expectedComparisonResult == ExpectComparisonTo.Fail ? todo : ok;
    634 
    635    // Compare animated value with expected
    636    var actualValue = normalize(actualStr);
    637    // Note: the actualStr should be empty string when using todoCompositor, so
    638    // actualValue is null in this case. However, compare() should handle null
    639    // well.
    640    okOrTodo(
    641      compare(expectedValue, actualValue, tolerance),
    642      desc +
    643        " - got " +
    644        actualStr +
    645        ", expected " +
    646        normalizedToString(expectedValue)
    647    );
    648 
    649    // For transform-like properties, if we have multiple transform-like
    650    // properties, the OMTA value and getComputedStyle() must be different,
    651    // so use this flag to skip the following tests.
    652    // FIXME: Putting this property on the expected value is a little bit odd.
    653    // It's not really a product of the expected value, but rather the kind of
    654    // test we're running. That said, the omta_is, omta_todo_is etc. methods are
    655    // already pretty complex and adding another parameter would probably
    656    // complicate things too much so this is fine for now. If we extend these
    657    // functions any more, though, we should probably reconsider this API.
    658    if (expected.usesMultipleProperties) {
    659      return;
    660    }
    661 
    662    if (typeof expected.computed !== "undefined") {
    663      // For some tests we specify a separate computed value for comparing
    664      // with getComputedStyle.
    665      //
    666      // In particular, we do this for the individual transform functions since
    667      // the form returned from getComputedStyle() reflects the individual
    668      // properties (e.g. 'translate: 100px') while the form we read back from
    669      // the compositor represents the combined result of all the transform
    670      // properties as a single transform matrix (e.g. [0, 0, 0, 0, 100, 0]).
    671      //
    672      // Despite the fact that we can't directly compare the OMTA value against
    673      // the getComputedStyle value in this case, it is still worth checking the
    674      // result of getComputedStyle since it will help to alert us if some
    675      // discrepancy arises between the way we calculate values on the main
    676      // thread and compositor.
    677      okOrTodo(
    678        computedStr == expected.computed,
    679        desc + ": Computed style should be equal to " + expected.computed
    680      );
    681    } else if (actualStr === compositorStr) {
    682      // For compositor animations do an additional check that they match
    683      // the value calculated on the main thread
    684      var computedValue = normalize(computedStr);
    685      if (computedValue === null) {
    686        ok(
    687          false,
    688          desc +
    689            ": test framework should parse computed style" +
    690            " - got " +
    691            computedStr
    692        );
    693        return;
    694      }
    695      okOrTodo(
    696        compare(computedValue, actualValue, 0.0),
    697        desc +
    698          ": OMTA style and computed style should be equal" +
    699          " - OMTA " +
    700          actualStr +
    701          ", computed " +
    702          computedStr
    703      );
    704    }
    705  };
    706 
    707  window.matricesRoughlyEqual = function (a, b, tolerance) {
    708    // Error handle if a or b is invalid.
    709    if (!a || !b) {
    710      return false;
    711    }
    712 
    713    tolerance = tolerance || 0.00011;
    714    for (var i = 0; i < 4; i++) {
    715      for (var j = 0; j < 4; j++) {
    716        var diff = Math.abs(a[i][j] - b[i][j]);
    717        if (diff > tolerance || isNaN(diff)) {
    718          return false;
    719        }
    720      }
    721    }
    722    return true;
    723  };
    724 
    725  // Converts something representing an transform into a 3d matrix in
    726  // column-major order.
    727  // The following are supported:
    728  //  "matrix(...)"
    729  //  "matrix3d(...)"
    730  //  [ 1, 0, 0, ... ]
    731  //  { a: 1, ty: 23 } etc.
    732  window.convertTo3dMatrix = function (matrixLike) {
    733    if (typeof matrixLike == "string") {
    734      return convertStringTo3dMatrix(matrixLike);
    735    } else if (Array.isArray(matrixLike)) {
    736      return convertArrayTo3dMatrix(matrixLike);
    737    } else if (typeof matrixLike == "object") {
    738      return convertObjectTo3dMatrix(matrixLike);
    739    }
    740    return null;
    741  };
    742 
    743  // In future most of these methods should be able to be replaced
    744  // with DOMMatrix
    745  window.isInvertible = function (matrix) {
    746    return getDeterminant(matrix) != 0;
    747  };
    748 
    749  // Converts strings of the format "matrix(...)" and "matrix3d(...)" to a 3d
    750  // matrix
    751  function convertStringTo3dMatrix(str) {
    752    if (str == "none") {
    753      return convertArrayTo3dMatrix([1, 0, 0, 1, 0, 0]);
    754    }
    755    var result = str.match("^matrix(3d)?\\(");
    756    if (result === null) {
    757      return null;
    758    }
    759 
    760    return convertArrayTo3dMatrix(
    761      str
    762        .substring(result[0].length, str.length - 1)
    763        .split(",")
    764        .map(function (component) {
    765          return Number(component);
    766        })
    767    );
    768  }
    769 
    770  // Takes an array of numbers of length 6 (2d matrix) or 16 (3d matrix)
    771  // representing a matrix specified in column-major order and returns a 3d
    772  // matrix represented as an array of arrays
    773  function convertArrayTo3dMatrix(array) {
    774    if (array.length == 6) {
    775      return convertObjectTo3dMatrix({
    776        a: array[0],
    777        b: array[1],
    778        c: array[2],
    779        d: array[3],
    780        e: array[4],
    781        f: array[5],
    782      });
    783    } else if (array.length == 16) {
    784      return [
    785        array.slice(0, 4),
    786        array.slice(4, 8),
    787        array.slice(8, 12),
    788        array.slice(12, 16),
    789      ];
    790    }
    791    return null;
    792  }
    793 
    794  // Return the first defined value in args.
    795  function defined(...args) {
    796    return args.find(arg => typeof arg !== "undefined");
    797  }
    798 
    799  // Takes an object of the form { a: 1.1, e: 23 } and builds up a 3d matrix
    800  // with unspecified values filled in with identity values.
    801  function convertObjectTo3dMatrix(obj) {
    802    return [
    803      [
    804        defined(obj.a, obj.sx, obj.m11, 1),
    805        obj.b || obj.m12 || 0,
    806        obj.m13 || 0,
    807        obj.m14 || 0,
    808      ],
    809      [
    810        obj.c || obj.m21 || 0,
    811        defined(obj.d, obj.sy, obj.m22, 1),
    812        obj.m23 || 0,
    813        obj.m24 || 0,
    814      ],
    815      [obj.m31 || 0, obj.m32 || 0, defined(obj.sz, obj.m33, 1), obj.m34 || 0],
    816      [
    817        obj.e || obj.tx || obj.m41 || 0,
    818        obj.f || obj.ty || obj.m42 || 0,
    819        obj.tz || obj.m43 || 0,
    820        defined(obj.m44, 1),
    821      ],
    822    ];
    823  }
    824 
    825  function convert3dMatrixToString(matrix) {
    826    if (is2d(matrix)) {
    827      return (
    828        "matrix(" +
    829        [
    830          matrix[0][0],
    831          matrix[0][1],
    832          matrix[1][0],
    833          matrix[1][1],
    834          matrix[3][0],
    835          matrix[3][1],
    836        ].join(", ") +
    837        ")"
    838      );
    839    }
    840    return (
    841      "matrix3d(" +
    842      matrix
    843        .reduce(function (outer, inner) {
    844          return outer.concat(inner);
    845        })
    846        .join(", ") +
    847      ")"
    848    );
    849  }
    850 
    851  function is2d(matrix) {
    852    return (
    853      matrix[0][2] === 0 &&
    854      matrix[0][3] === 0 &&
    855      matrix[1][2] === 0 &&
    856      matrix[1][3] === 0 &&
    857      matrix[2][0] === 0 &&
    858      matrix[2][1] === 0 &&
    859      matrix[2][2] === 1 &&
    860      matrix[2][3] === 0 &&
    861      matrix[3][2] === 0 &&
    862      matrix[3][3] === 1
    863    );
    864  }
    865 
    866  function getDeterminant(matrix) {
    867    if (is2d(matrix)) {
    868      return matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0];
    869    }
    870 
    871    return (
    872      matrix[0][3] * matrix[1][2] * matrix[2][1] * matrix[3][0] -
    873      matrix[0][2] * matrix[1][3] * matrix[2][1] * matrix[3][0] -
    874      matrix[0][3] * matrix[1][1] * matrix[2][2] * matrix[3][0] +
    875      matrix[0][1] * matrix[1][3] * matrix[2][2] * matrix[3][0] +
    876      matrix[0][2] * matrix[1][1] * matrix[2][3] * matrix[3][0] -
    877      matrix[0][1] * matrix[1][2] * matrix[2][3] * matrix[3][0] -
    878      matrix[0][3] * matrix[1][2] * matrix[2][0] * matrix[3][1] +
    879      matrix[0][2] * matrix[1][3] * matrix[2][0] * matrix[3][1] +
    880      matrix[0][3] * matrix[1][0] * matrix[2][2] * matrix[3][1] -
    881      matrix[0][0] * matrix[1][3] * matrix[2][2] * matrix[3][1] -
    882      matrix[0][2] * matrix[1][0] * matrix[2][3] * matrix[3][1] +
    883      matrix[0][0] * matrix[1][2] * matrix[2][3] * matrix[3][1] +
    884      matrix[0][3] * matrix[1][1] * matrix[2][0] * matrix[3][2] -
    885      matrix[0][1] * matrix[1][3] * matrix[2][0] * matrix[3][2] -
    886      matrix[0][3] * matrix[1][0] * matrix[2][1] * matrix[3][2] +
    887      matrix[0][0] * matrix[1][3] * matrix[2][1] * matrix[3][2] +
    888      matrix[0][1] * matrix[1][0] * matrix[2][3] * matrix[3][2] -
    889      matrix[0][0] * matrix[1][1] * matrix[2][3] * matrix[3][2] -
    890      matrix[0][2] * matrix[1][1] * matrix[2][0] * matrix[3][3] +
    891      matrix[0][1] * matrix[1][2] * matrix[2][0] * matrix[3][3] +
    892      matrix[0][2] * matrix[1][0] * matrix[2][1] * matrix[3][3] -
    893      matrix[0][0] * matrix[1][2] * matrix[2][1] * matrix[3][3] -
    894      matrix[0][1] * matrix[1][0] * matrix[2][2] * matrix[3][3] +
    895      matrix[0][0] * matrix[1][1] * matrix[2][2] * matrix[3][3]
    896    );
    897  }
    898 })();
    899 
    900 //----------------------------------------------------------------------
    901 //
    902 // Promise wrappers for paint_listener.js
    903 //
    904 //----------------------------------------------------------------------
    905 
    906 // Returns a Promise that resolves once all paints have completed
    907 function waitForPaints() {
    908  return new Promise(function (resolve, reject) {
    909    waitForAllPaints(resolve);
    910  });
    911 }
    912 
    913 // As with waitForPaints but also flushes pending style changes before waiting
    914 function waitForPaintsFlushed() {
    915  return new Promise(function (resolve, reject) {
    916    waitForAllPaintsFlushed(resolve);
    917  });
    918 }
    919 
    920 function waitForVisitedLinkColoring(visitedLink, waitProperty, waitValue) {
    921  function checkLink(resolve) {
    922    if (
    923      SpecialPowers.DOMWindowUtils.getVisitedDependentComputedStyle(
    924        visitedLink,
    925        "",
    926        waitProperty
    927      ) == waitValue
    928    ) {
    929      // Our link has been styled as visited.  Resolve.
    930      resolve(true);
    931    } else {
    932      // Our link is not yet styled as visited.  Poll for completion.
    933      setTimeout(checkLink, 0, resolve);
    934    }
    935  }
    936  return new Promise(function (resolve, reject) {
    937    checkLink(resolve);
    938  });
    939 }