gecko/layout/style/test/test_transitions.html

763 lines
27 KiB
HTML

<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=435441
-->
<head>
<title>Test for Bug 435441</title>
<script type="application/javascript" src="/MochiKit/packed.js"></script>
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.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;
-moz-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 **/
function px_to_num(str)
{
return Number(String(str).match(/^([\d.]+)px$/)[1]);
}
// Run tests simultaneously so we don't have to take up too much time.
SimpleTest.waitForExplicitFinish();
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();
}
}
function bezier(x1, y1, x2, y2) {
// Cubic bezier with control points (0, 0), (x1, y1), (x2, y2), and (1, 1).
function x_for_t(t) {
var omt = 1-t;
return 3 * omt * omt * t * x1 + 3 * omt * t * t * x2 + t * t * t;
}
function y_for_t(t) {
var omt = 1-t;
return 3 * omt * omt * t * y1 + 3 * omt * t * t * y2 + t * t * t;
}
function t_for_x(x) {
// Binary subdivision.
var mint = 0, maxt = 1;
for (var i = 0; i < 30; ++i) {
var guesst = (mint + maxt) / 2;
var guessx = x_for_t(guesst);
if (x < guessx)
maxt = guesst;
else
mint = guesst;
}
return (mint + maxt) / 2;
}
return function bezier_closure(x) {
if (x == 0) return 0;
if (x == 1) return 1;
return y_for_t(t_for_x(x));
}
}
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 start transitions.
// We have two reference elements to check the expected timing range.
// They both have 8s linear transitions from 0 to 1000px.
function make_reference_p() {
var p = document.createElement("p");
p.appendChild(document.createTextNode("reference"));
p.style.textIndent = "0px";
p.style.MozTransition = "8s 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.MozTransition = "8s text-indent linear";
p.style.MozTransitionTimingFunction = 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 each (var restyleParent in [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.MozTransition = "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.MozTransition = "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 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.MozTransition = 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 do not trigger 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 is ignored (as part of the idea of not
// starting transitions on changes that result from animation).
// See http://lists.w3.org/Archives/Public/www-style/2009Jun/0121.html
// and http://lists.w3.org/Archives/Public/www-style/2009Jul/0050.html
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.MozTransition = test.parent_transition;
test.childNode.style.MozTransition = 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: "-moz-transition: 4s margin, 8s margin-left" },
{ style: "-moz-transition: 4s margin-left, 8s margin" },
{ style: "-moz-transition-property: margin-left; " +
"-moz-transition-duration: 8s, 2s" },
{ style: "-moz-transition-property: margin-left, margin-left; " +
"-moz-transition-duration: 2s, 8s" },
{ style: "-moz-transition-property: margin-left, margin-left, margin-left; " +
"-moz-transition-duration: 8s, 2s" },
{ style: "-moz-transition-property: margin-left; " +
"-moz-transition-duration: 8s, 16s" },
{ style: "-moz-transition-property: margin-left, margin-left; " +
"-moz-transition-duration: 16s, 8s" },
{ style: "-moz-transition-property: margin-left, margin-left, margin-left; " +
"-moz-transition-duration: 8s, 16s" },
{ style: "-moz-transition-property: text-indent,word-spacing,margin-left; " +
"-moz-transition-duration: 8s; " +
"-moz-transition-delay: 0, 8s" },
{ style: "-moz-transition-property: text-indent,word-spacing,margin-left; " +
"-moz-transition-duration: 8s, 16s; " +
"-moz-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.MozTransition = "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";
}
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)
{
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.0015;
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) / 125,
px_to_num(laterefcs.textIndent) / 125 ];
// 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) / 8,
(Date.now() - gStartTime1 + 20) / 8 ];
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]/125 + "s and " + expected_range[1]/125 + "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 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"];
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)");
}
if (property != "letter-spacing" && child_ti_duration == 0) {
child_ti_duration = duration;
child_ti_delay = delay;
}
if (property != "text-indent" && child_ls_duration == 0) {
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, 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)
{
var tf = timingFunctions["ease-in-out"];
for (var i in display_tests) {
var p = display_tests[i];
check_transition_value(tf, 0, 8, 0, 100,
getComputedStyle(p, "").textIndent,
"display test for test with " +
p.childNodes[0].data,
// TODO: Making transitions work on 'display:none' elements is
// still not implemented.
function(range) { return p != to_none_test &&
range[1] < 100 });
}
}
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>