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