<!DOCTYPE HTML> <html> <!-- https://bugzilla.mozilla.org/show_bug.cgi?id=435441 --> <head> <title>Test for Bug 435441</title> <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> <script type="application/javascript" src="animation_utils.js"></script> <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> <style type="text/css"> #display p { margin-top: 0; margin-bottom: 0; } #display .before, #display .after { width: -moz-fit-content; border: 1px solid black; } #display .before::before, #display .after::after { display: block; width: 0; text-indent: 0; } #display .before.started::before, #display .after.started::after { width: 100px; text-indent: 100px; transition: 8s width ease-in-out, 8s text-indent ease-in-out; } #display .before::before { content: "Before"; } #display .after::after { content: "After"; } </style> </head> <body> <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=435441">Mozilla Bug 435441</a> <div id="display"> </div> <pre id="test"> <script type="application/javascript"> /** Test for Bug 435441 **/ // Run tests simultaneously so we don't have to take up too much time. SimpleTest.waitForExplicitFinish(); SimpleTest.requestFlakyTimeout("untriaged"); var gTestsRunning = 0; function TestStarted() { ++gTestsRunning; } function TestFinished() { if (--gTestsRunning == 0) SimpleTest.finish(); } // An array of arrays of functions to be called at the outer index number // of seconds after the present. var gFutureCalls = []; function add_future_call(index, func) { if (!(index in gFutureCalls)) { gFutureCalls[index] = []; } gFutureCalls[index].push(func); TestStarted(); } var gStartTime1, gStartTime2; var gCurrentTime; var gSetupComplete = false; function process_future_calls(index) { var calls = gFutureCalls[index]; if (!calls) return; gCurrentTime = Date.now(); for (var i = 0; i < calls.length; ++i) { calls[i](); TestFinished(); } } var timingFunctions = { // a map from the value of 'transition-timing-function' to an array of // the portions this function yields at 0 (always 0), 1/4, 1/2, and // 3/4 and all (always 1) of the way through the time of the // transition. Each portion is represented as a value and an // acceptable error tolerance (based on a time error of 1%) for that // value. // ease "ease": bezier(0.25, 0.1, 0.25, 1), "cubic-bezier(0.25, 0.1, 0.25, 1.0)": bezier(0.25, 0.1, 0.25, 1), // linear and various synonyms for it "linear": function(x) { return x; }, "cubic-bezier(0.0, 0.0, 1.0, 1.0)": function(x) { return x; }, "cubic-bezier(0, 0, 1, 1)": function(x) { return x; }, "cubic-bezier(0, 0, 0, 0.0)": function(x) { return x; }, "cubic-bezier(1.0, 1, 0, 0)": function(x) { return x; }, // ease-in "ease-in": bezier(0.42, 0, 1, 1), "cubic-bezier(0.42, 0, 1.0, 1.0)": bezier(0.42, 0, 1, 1), // ease-out "ease-out": bezier(0, 0, 0.58, 1), "cubic-bezier(0, 0, 0.58, 1.0)": bezier(0, 0, 0.58, 1), // ease-in-out "ease-in-out": bezier(0.42, 0, 0.58, 1), "cubic-bezier(0.42, 0, 0.58, 1.0)": bezier(0.42, 0, 0.58, 1), // other cubic-bezier values "cubic-bezier(0.4, 0.1, 0.7, 0.95)": bezier(0.4, 0.1, 0.7, 0.95), "cubic-bezier(1, 0, 0, 1)": bezier(1, 0, 0, 1), "cubic-bezier(0, 1, 1, 0)": bezier(0, 1, 1, 0), }; var div = document.getElementById("display"); // Set up all the elements on which we are going to initiate transitions. // We have two reference elements to check the expected timing range. // They both have 16s linear transitions from 0 to 1000px. // This means they move through 62.5 pixels per second. const REF_PX_PER_SEC = 62.5; function make_reference_p() { var p = document.createElement("p"); p.appendChild(document.createTextNode("reference")); p.style.textIndent = "0px"; p.style.transition = "16s text-indent linear"; div.appendChild(p); return p; } var earlyref = make_reference_p(); var earlyrefcs = getComputedStyle(earlyref, ""); // Test all timing functions using a set of 8-second transitions, which // we check at times 0, 2s, 4s, 6s, and 8s. var tftests = []; for (var tf in timingFunctions) { var p = document.createElement("p"); var t = document.createTextNode("transition-timing-function: " + tf); p.appendChild(t); p.style.textIndent = "0px"; p.style.transition = "8s text-indent linear"; p.style.transitionTimingFunction = tf; div.appendChild(p); is(getComputedStyle(p, "").textIndent, "0px", "should be zero before changing value"); tftests.push([ p, tf ]); } // Check that the timing function continues even when we restyle in the // middle. var interrupt_tests = []; for (var restyleParent of [true, false]) { for (var itime = 2; itime < 8; itime += 2) { var p = document.createElement("p"); var t = document.createTextNode("interrupt on " + (restyleParent ? "parent" : "node itself") + " at " + itime + "s"); p.appendChild(t); p.style.textIndent = "0px"; p.style.transition = "8s text-indent cubic-bezier(0, 1, 1, 0)"; if (restyleParent) { var d = document.createElement("div"); d.appendChild(p); div.appendChild(d); } else { div.appendChild(p); } is(getComputedStyle(p, "").textIndent, "0px", "should be zero before changing value"); setTimeout("interrupt_tests[" + interrupt_tests.length + "]" + "[0]" + (restyleParent ? ".parentNode" : "") + ".style.color = 'blue';" + "check_interrupt_tests()", itime*1000); interrupt_tests.push([ p, itime ]); } } // Test transition-delay values of -4s through 4s on a 4s transition // with 'ease-out' timing function. var delay_tests = {}; for (var d = -4; d <= 4; ++d) { var p = document.createElement("p"); var delay = d + "s"; var t = document.createTextNode("transition-delay: " + delay); p.appendChild(t); p.style.marginLeft = "0px"; p.style.transition = "4s margin-left ease-out " + delay; div.appendChild(p); is(getComputedStyle(p, "").marginLeft, "0px", "should be zero before changing value"); delay_tests[d] = p; } // Test transition-delay values of -4s through 4s on a 4s transition // with duration of zero. var delay_zero_tests = {}; for (var d = -4; d <= 4; ++d) { var p = document.createElement("p"); var delay = d + "s"; var t = document.createTextNode("transition-delay: " + delay); p.appendChild(t); p.style.marginLeft = "0px"; p.style.transition = "0s margin-left linear " + delay; div.appendChild(p); is(getComputedStyle(p, "").marginLeft, "0px", "should be zero before changing value"); delay_zero_tests[d] = p; } // Test that changing the value on an already-running transition to the // value it currently happens to have resets the transition. function make_reset_test(transition, description) { var p = document.createElement("p"); var t = document.createTextNode(description); p.appendChild(t); p.style.marginLeft = "0px"; p.style.transition = transition; div.appendChild(p); is(getComputedStyle(p, "").marginLeft, "0px", "should be zero before changing value"); return p; } var reset_test = make_reset_test("4s margin-left ease-out 4s", "transition-delay reset to starting point"); var reset_test_reference = make_reset_test("4s margin-left linear -3s", "reference for previous test (reset test)"); // Test that transitions on descendants start correctly when the // inherited value is itself transitioning. In other words, when // ancestor and descendant both have a transition for the same property, // and the descendant inherits the property from the ancestor, the // descendant's transition starts as specified, based on the concepts of // the before-change style, the after-change style, and the // after-transition style. var descendant_tests = [ { parent_transition: "", child_transition: "4s text-indent" }, { parent_transition: "4s text-indent", child_transition: "" }, { parent_transition: "4s text-indent", child_transition: "16s text-indent" }, { parent_transition: "4s text-indent", child_transition: "1s text-indent" }, { parent_transition: "8s letter-spacing", child_transition: "4s text-indent" }, { parent_transition: "4s text-indent", child_transition: "8s letter-spacing" }, { parent_transition: "4s text-indent", child_transition: "8s all" }, { parent_transition: "8s text-indent", child_transition: "4s all" }, // examples with positive and negative delay { parent_transition: "4s text-indent 1s", child_transition: "8s text-indent" }, { parent_transition: "4s text-indent -1s", child_transition: "8s text-indent" } ]; for (var i in descendant_tests) { var test = descendant_tests[i]; test.parentNode = document.createElement("div"); test.childNode = document.createElement("p"); test.parentNode.appendChild(test.childNode); test.childNode.appendChild(document.createTextNode( "parent with \"" + test.parent_transition + "\" and " + "child with \"" + test.child_transition + "\"")); test.parentNode.style.transition = test.parent_transition; test.childNode.style.transition = test.child_transition; test.parentNode.style.textIndent = "50px"; // transition from 50 to 150 test.parentNode.style.letterSpacing = "10px"; // transition from 10 to 5 div.appendChild(test.parentNode); var parentCS = getComputedStyle(test.parentNode, ""); var childCS = getComputedStyle(test.childNode, ""); is(parentCS.textIndent, "50px", "parent text-indent should be 50px before changing"); is(parentCS.letterSpacing, "10px", "parent letter-spacing should be 10px before changing"); is(childCS.textIndent, "50px", "child text-indent should be 50px before changing"); is(childCS.letterSpacing, "10px", "child letter-spacing should be 10px before changing"); test.childCS = childCS; } // For all of these transitions, the transition for margin-left should // have a duration of 8s, and the default timing function (ease) and // delay (0). // This is because we're implementing the proposal in // http://lists.w3.org/Archives/Public/www-style/2009Aug/0109.html var number_tests = [ { style: "transition: 4s margin, 8s margin-left" }, { style: "transition: 4s margin-left, 8s margin" }, { style: "transition-property: margin-left; " + "transition-duration: 8s, 2s" }, { style: "transition-property: margin-left, margin-left; " + "transition-duration: 2s, 8s" }, { style: "transition-property: margin-left, margin-left, margin-left; " + "transition-duration: 8s, 2s" }, { style: "transition-property: margin-left; " + "transition-duration: 8s, 16s" }, { style: "transition-property: margin-left, margin-left; " + "transition-duration: 16s, 8s" }, { style: "transition-property: margin-left, margin-left, margin-left; " + "transition-duration: 8s, 16s" }, { style: "transition-property: text-indent,word-spacing,margin-left; " + "transition-duration: 8s; " + "transition-delay: 0, 8s" }, { style: "transition-property: text-indent,word-spacing,margin-left; " + "transition-duration: 8s, 16s; " + "transition-delay: 8s, 8s, 0, 8s, 8s, 8s" }, ]; for (var i in number_tests) { var test = number_tests[i]; var p = document.createElement("p"); p.setAttribute("style", test.style); var t = document.createTextNode(test.style); p.appendChild(t); p.style.marginLeft = "100px"; div.appendChild(p); is(getComputedStyle(p, "").marginLeft, "100px", "should be 100px before changing value"); test.node = p; } // Test transitions that are also from-display:none, to-display:none, and // display:none throughout. var from_none_test, to_none_test, always_none_test; function make_display_test(initially_none, text) { var p = document.createElement("p"); p.appendChild(document.createTextNode(text)); p.style.textIndent = "0px"; p.style.transition = "8s text-indent ease-in-out"; if (initially_none) p.style.display = "none"; div.appendChild(p); return p; } from_none_test = make_display_test(true, "transition from display:none"); to_none_test = make_display_test(false, "transition to display:none"); always_none_test = make_display_test(true, "transition always display:none"); var display_tests = [ from_none_test, to_none_test, always_none_test ]; // Test transitions on pseudo-elements var before_test, after_test; function make_pseudo_elem_test(pseudo) { var p = document.createElement("p"); p.className = pseudo; div.appendChild(p); return {"pseudo": pseudo, element: p}; } before_test = make_pseudo_elem_test("before"); after_test = make_pseudo_elem_test("after"); var pseudo_element_tests = [ before_test, after_test ]; // FIXME (Bug 522599): Test a transition that reverses partway through. var lateref = make_reference_p(); var laterefcs = getComputedStyle(lateref, ""); // flush style changes var x = getComputedStyle(div, "").color; // Start our timer as close as possible to when we start the first // transition. // Do not use setInterval because once it gets off in time, it stays off. for (var i = 1; i <= 8; ++i) { setTimeout(process_future_calls, i * 1000, i); } gStartTime1 = Date.now(); // set before any transitions have started // Start all the transitions. earlyref.style.textIndent = "1000px"; for (var test in tftests) { var p = tftests[test][0]; p.style.textIndent = "100px"; } for (var test in interrupt_tests) { var p = interrupt_tests[test][0]; p.style.textIndent = "100px"; } for (var d in delay_tests) { var p = delay_tests[d]; p.style.marginLeft = "100px"; } for (var d in delay_zero_tests) { var p = delay_zero_tests[d]; p.style.marginLeft = "100px"; } reset_test.style.marginLeft = "100px"; reset_test_reference.style.marginLeft = "100px"; for (var i in descendant_tests) { var test = descendant_tests[i]; test.parentNode.style.textIndent = "150px"; test.parentNode.style.letterSpacing = "5px"; } for (var i in number_tests) { var test = number_tests[i]; test.node.style.marginLeft = "50px"; } from_none_test.style.textIndent = "100px"; from_none_test.style.display = ""; to_none_test.style.textIndent = "100px"; to_none_test.style.display = "none"; always_none_test.style.textIndent = "100px"; for (var i in pseudo_element_tests) { var test = pseudo_element_tests[i]; test.element.classList.add("started"); } lateref.style.textIndent = "1000px"; // flush style changes x = getComputedStyle(div, "").color; gStartTime2 = Date.now(); // set after all transitions have started gCurrentTime = gStartTime2; /** * Assert that a transition whose timing function yields the bezier * |func|, running from |start_time| to |end_time| (both in seconds * relative to when the transitions were started) should have produced * computed value |cval| given that the transition was from * |start_value| to |end_value| (both numbers in CSS pixels). */ function check_transition_value(func, start_time, end_time, start_value, end_value, cval, desc, xfail) { /** * Compute the value at a given time |elapsed|, by normalizing the * input to the timing function using start_time and end_time and * then turning the output into a value using start_value and * end_value. * * The |error_direction| argument should be either -1, 0, or 1, * suggesting adding on a little bit of error, to allow for the * cubic-bezier calculation being an approximation. The amount of * error is proportional to the slope of the timing function, since * the error is added to the *input* of the timing function (after * normalization to 0-1 based on start_time and end_time). */ function value_at(elapsed, error_direction) { var time_portion = (elapsed - start_time) / (end_time - start_time); if (time_portion < 0) time_portion = 0; else if (time_portion > 1) time_portion = 1; // Assume a small error since bezier computation can be off slightly. // (This test's computation is probably more accurate than Mozilla's.) var value_portion = func(time_portion + error_direction * 0.0005); if (value_portion < 0) value_portion = 0; else if (value_portion > 1) value_portion = 1; var value = (1 - value_portion) * start_value + value_portion * end_value; if (start_value > end_value) error_direction = -error_direction; // Computed values get rounded to 1/60th of a pixel. return value + error_direction * 0.02; } var time_range; // in seconds var uns_range; // |range| before being sorted (so errors give it // in the original order if (!gSetupComplete) { // No timers involved time_range = [0, 0]; if (start_time < 0) { uns_range = [ value_at(0, -1), value_at(0, 1) ]; } else { var val = value_at(0, 0); uns_range = [val, val]; } } else { time_range = [ px_to_num(earlyrefcs.textIndent) / REF_PX_PER_SEC, px_to_num(laterefcs.textIndent) / REF_PX_PER_SEC ]; // seconds uns_range = [ value_at(time_range[0], -1), value_at(time_range[1], 1) ]; } var range = uns_range.concat(). /* concat to clone array */ sort(function compareNumbers(a,b) { return a - b; }); var actual = px_to_num(cval); var fn = ok; if (xfail && xfail(range)) fn = todo; fn(range[0] <= actual && actual <= range[1], desc + ": computed value " + cval + " should be between " + uns_range[0].toFixed(6) + "px and " + uns_range[1].toFixed(6) + "px at time between " + time_range[0] + "s and " + time_range[1] + "s."); } function check_ref_range() { // This is the only test where we compare the progress of the // transitions to an actual time; we need considerable tolerance at // the low end (we are using half a second). var expected_range = [ (gCurrentTime - gStartTime2 - 40) / 16, (Date.now() - gStartTime1 + 20) / 16 ]; if (expected_range[0] > 1000) { expected_range[0] = 1000; } if (expected_range[1] > 1000) { expected_range[1] = 1000; } function check(desc, value) { // The timing on the unit test VMs is not reliable, so make this // test report PASS when it succeeds and TODO when it fails. var passed = expected_range[0] <= value && value <= expected_range[1]; (passed ? ok : todo)(passed, desc + ": computed value " + value + "px should be between " + expected_range[0].toFixed(6) + "px and " + expected_range[1].toFixed(6) + "px at time between " + expected_range[0]/REF_PX_PER_SEC + "s and " + expected_range[1]/REF_PX_PER_SEC + "s."); } check("early reference", px_to_num(earlyrefcs.textIndent)); check("late reference", px_to_num(laterefcs.textIndent)); } for (var i = 1; i <= 8; ++i) { add_future_call(i, check_ref_range); } function check_tf_test() { for (var test in tftests) { var p = tftests[test][0]; var tf = tftests[test][1]; check_transition_value(timingFunctions[tf], 0, 8, 0, 100, getComputedStyle(p, "").textIndent, "timing function test for timing function " + tf); } check_interrupt_tests(); } check_tf_test(); add_future_call(2, check_tf_test); add_future_call(4, check_tf_test); add_future_call(6, check_tf_test); add_future_call(8, check_tf_test); function check_interrupt_tests() { for (var test in interrupt_tests) { var p = interrupt_tests[test][0]; var itime = interrupt_tests[test][1]; check_transition_value(timingFunctions["cubic-bezier(0, 1, 1, 0)"], 0, 8, 0, 100, getComputedStyle(p, "").textIndent, "interrupt " + (p.parentNode == div ? "" : "on parent ") + "test for time " + itime + "s"); } } // check_interrupt_tests is called from check_tf_test and from // where we reset the interrupts function check_delay_test(time) { var tf = timingFunctions["ease-out"]; for (var d in delay_tests) { var p = delay_tests[d]; check_transition_value(tf, Number(d), Number(d) + 4, 0, 100, getComputedStyle(p, "").marginLeft, "delay test for delay " + d + "s"); } } check_delay_test(0); for (var i = 1; i <= 8; ++i) { add_future_call(i, check_delay_test); } function check_delay_zero_test(time) { for (var d in delay_zero_tests) { var p = delay_zero_tests[d]; time_range = [ px_to_num(earlyrefcs.textIndent) / REF_PX_PER_SEC, px_to_num(laterefcs.textIndent) / REF_PX_PER_SEC ]; var m = getComputedStyle(p, "").marginLeft; var desc = "delay_zero test for delay " + d + "s"; if (time_range[0] < d && time_range[1] < d) { is(m, "0px", desc); } else if ((time_range[0] > d && time_range[1] > d) || (d == 0 && time == 0)) { is(m, "100px", desc); } } } check_delay_zero_test(0); for (var i = 1; i <= 8; ++i) { add_future_call(i, check_delay_zero_test); } function reset_reset_test(time) { reset_test.style.marginLeft = "0px"; } function check_reset_test(time) { is(getComputedStyle(reset_test, "").marginLeft, "0px", "reset test value at time " + time + "s."); } check_reset_test(0); // reset the reset test right now so we don't have to worry about clock skew // To make sure that this is valid, check that a pretty-much-identical test is // already transitioning. is(getComputedStyle(reset_test_reference, "").marginLeft, "75px", "reset test reference value"); reset_reset_test(); check_reset_test(0); for (var i = 1; i <= 8; ++i) { (function(j) { add_future_call(j, function() { check_reset_test(j); }); })(i); } check_descendant_tests(); add_future_call(2, check_descendant_tests); add_future_call(6, check_descendant_tests); function check_descendant_tests() { // text-indent: transition from 50px to 150px // letter-spacing: transition from 10px to 5px var values = {}; values["text-indent"] = [ 50, 150 ]; values["letter-spacing"] = [ 10, 5 ]; var tf = timingFunctions["ease"]; var time = px_to_num(earlyrefcs.textIndent) / REF_PX_PER_SEC; for (var i in descendant_tests) { var test = descendant_tests[i]; /* ti=text-indent, ls=letter-spacing */ var child_ti_duration = 0; var child_ls_duration = 0; var child_ti_delay = 0; var child_ls_delay = 0; if (test.parent_transition != "") { var props = test.parent_transition.split(" "); var duration = parseInt(props[0]); var delay = (props.length > 2) ? parseInt(props[2]) : 0; var property = props[1]; if (property == "text-indent") { child_ti_duration = duration; child_ti_delay = delay; } else if (property == "letter-spacing") { child_ls_duration = duration; child_ls_delay = delay; } else { ok(false, "fix this test (unexpected transition-property " + property + " on parent)"); } } if (test.child_transition != "") { var props = test.child_transition.split(" "); var duration = parseInt(props[0]); var delay = (props.length > 2) ? parseInt(props[2]) : 0; var property = props[1]; if (property != "text-indent" && property != "letter-spacing" && property != "all") { ok(false, "fix this test (unexpected transition-property " + property + " on child)"); } // Override the parent's transition with the child's as long // as the child transition is still running. if (property != "letter-spacing" && duration + delay > time) { child_ti_duration = duration; child_ti_delay = delay; } if (property != "text-indent" && duration + delay > time) { child_ls_duration = duration; child_ls_delay = delay; } } var time_portions = { "text-indent": { duration: child_ti_duration, delay: child_ti_delay }, "letter-spacing": { duration: child_ls_duration, delay: child_ls_delay }, }; for (var prop in {"text-indent": true, "letter-spacing": true}) { var time_portion = time_portions[prop]; if (time_portion.duration == 0) { time_portion.duration = 0.01; time_portion.delay = -1; } check_transition_value(tf, time_portion.delay, time_portion.delay + time_portion.duration, values[prop][0], values[prop][1], test.childCS.getPropertyValue(prop), `descendant test #${Number(i)+1}, property ${prop}`); } } } function check_number_tests() { var tf = timingFunctions["ease"]; for (var d in number_tests) { var test = number_tests[d]; var p = test.node; check_transition_value(tf, 0, 8, 100, 50, getComputedStyle(p, "").marginLeft, "number of transitions test for style " + test.style); } } check_number_tests(0); add_future_call(2, check_number_tests); add_future_call(4, check_number_tests); add_future_call(6, check_number_tests); add_future_call(8, check_number_tests); function check_display_tests(time) { for (var i in display_tests) { var p = display_tests[i]; // There is no transition if the old or new style is display:none, so // the computed value is always the end value. var computedValue = getComputedStyle(p, "").textIndent; is(computedValue, "100px", "display test for test with " + p.childNodes[0].data + ": computed value " + computedValue + " should be 100px."); } } check_display_tests(0); add_future_call(2, function() { check_display_tests(2); }); add_future_call(4, function() { check_display_tests(4); }); add_future_call(6, function() { check_display_tests(6); }); add_future_call(8, function() { check_display_tests(8); }); function check_pseudo_element_tests(time) { var tf = timingFunctions["ease-in-out"]; for (var i in pseudo_element_tests) { var test = pseudo_element_tests[i]; check_transition_value(tf, 0, 8, 0, 100, getComputedStyle(test.element, "").width, "::"+test.pseudo+" test"); check_transition_value(tf, 0, 8, 0, 100, getComputedStyle(test.element, "::"+test.pseudo).textIndent, "::"+test.pseudo+" indent test"); } } check_pseudo_element_tests(0); add_future_call(2, function() { check_pseudo_element_tests(2); }); add_future_call(4, function() { check_pseudo_element_tests(4); }); add_future_call(6, function() { check_pseudo_element_tests(6); }); add_future_call(8, function() { check_pseudo_element_tests(8); }); gSetupComplete = true; </script> </pre> </body> </html>