contributors/aryehgregor/incoming/2d-transforms.html

Thu, 05 Jan 2012 13:38:01 -0700

author
Aryeh Gregor <ayg@aryeh.name>
date
Thu, 05 Jan 2012 13:38:01 -0700
changeset 2480
1f15a2a425ba
parent 2479
03072a301435
child 2481
6a19628a421c
permissions
-rw-r--r--

Revamp 2D Transforms tests

Now suitable for preliminary review, although there's still lots more to test.

     1 <!doctype html>
     2 <title>CSS 2D Transforms tests</title>
     3 <link rel=author title="Aryeh Gregor" href="ayg@aryeh.name">
     4 <script src=http://w3c-test.org/resources/testharness.js></script>
     5 <script src=http://w3c-test.org/resources/testharnessreport.js></script>
     6 <style>
     7 body { margin: 0 }
     8 #test { height: 50px; width: 50px; background: blue }
     9 </style>
    10 <div id=test></div>
    11 <div id=log></div>
    12 <script>
    13 // TODO: Test floats, non-static positioning, different display
    14 // (inline/table/etc.), both atomic and non-atomic inline elements, CSSMatrix,
    15 // interaction with SVG, nested transformations, overflow, creation of stacking
    16 // context/containing block, fixed backgrounds, specificity of SVG transform
    17 // attribute, multiple transformations
    18 //
    19 // Not for now: transitions, animations
    20 var div = document.querySelector("#test");
    21 var divWidth = 50, divHeight = 50;
    22 // Arbitrarily chosen epsilon that makes browsers mostly pass with some extra
    23 // breathing room, since the specs don't define rounding.
    24 var epsilon = 1.5;
    25 // Account for prefixing so that I can check whether browsers actually follow
    26 // the spec.  Obviously, in any final version of the test, only the unprefixed
    27 // property will be tested.
    28 var prop = "transform" in div.style ? "transform"
    29 	: "msTransform" in div.style ? "msTransform"
    30 	: "MozTransform" in div.style ? "MozTransform"
    31 	: "webkitTransform" in div.style ? "webkitTransform"
    32 	: "OTransform" in div.style ? "OTransform"
    33 	: undefined;
    35 /**
    36  * Tests that style="transform: value" results in transformation by the matrix
    37  * [a, b, c, d, e, f].  Checks both the computed value and bounding box.
    38  */
    39 function testTransform(value, a, b, c, d, e, f) {
    40 	testTransformParsing(value, a, b, c, d, e, f);
    41 	testTransformedBoundary(value, a, b, c, d, e, f, "50% 50%", divWidth/2, divHeight/2);
    42 }
    44 /**
    45  * Tests that style="transform: value" results in a computed value of
    46  * "matrix(a, b, c, d, e, f)".
    47  */
    48 function testTransformParsing(value, a, b, c, d, e, f) {
    49 	test(function() {
    50 		// TODO: test div.setAttribute("style", ...) too
    51 		div.style[prop] = value;
    52 		// FIXME: We allow px optionally in the last two entries because Gecko
    53 		// adds it while other engines don't, and the spec is unclear about
    54 		// which behavior is correct:
    55 		// https://www.w3.org/Bugs/Public/show_bug.cgi?id=15431
    56 		var computed = getComputedStyle(div)[prop];
    57 		var re = /^matrix\(([^,]+), ([^,]+), ([^,]+), ([^,]+), ([^,]+?)(?:px)?, ([^,]+?)(?:px)?\)$/;
    58 		assert_regexp_match(computed, re, "computed value has unexpected form");
    59 		var match = re.exec(computed);
    60 		assert_approx_equals(Number(match[1]), a, epsilon, "getComputedStyle first matrix component");
    61 		assert_approx_equals(Number(match[2]), b, epsilon, "getComputedStyle second matrix component");
    62 		assert_approx_equals(Number(match[3]), c, epsilon, "getComputedStyle third matrix component");
    63 		assert_approx_equals(Number(match[4]), d, epsilon, "getComputedStyle fourth matrix component");
    64 		assert_approx_equals(Number(match[5]), e, epsilon, "getComputedStyle fifth matrix component");
    65 		assert_approx_equals(Number(match[6]), f, epsilon, "getComputedStyle sixth matrix component");
    66 	}, "Computed value for transform: " + value);
    67 }
    69 /**
    70  * Tests that
    71  *   style="transform: transformValue; transform-origin: transformOriginValue"
    72  * results in the boundary box that you'd get from transforming with a matrix
    73  * of [a, b, c, d, e, f] around an offset of [xOffset, yOffset].
    74  */
    75 function testTransformedBoundary(transformValue, a, b, c, d, e, f,
    76                                  transformOriginValue, xOffset, yOffset) {
    77 	test(function() {
    78 		// TODO: test div.setAttribute("style", ...) too
    79 		div.style[prop] = transformValue;
    80 		div.style[prop + "Origin"] = transformOriginValue;
    82 		var originalPoints = [[0, 0], [0, divHeight], [divWidth, 0], [divWidth, divHeight]];
    83 		var expectedTop, expectedRight, expectedBottom, expectedLeft;
    84 		for (var i = 0; i < originalPoints.length; i++) {
    85 			var newX = a*(originalPoints[i][0]-xOffset) + c*(originalPoints[i][1]-yOffset) + e + xOffset;
    86 			var newY = b*(originalPoints[i][0]-xOffset) + d*(originalPoints[i][1]-yOffset) + f + yOffset;
    87 			if (expectedTop === undefined || newY < expectedTop) {
    88 				expectedTop = newY;
    89 			}
    90 			if (expectedRight === undefined || newX > expectedRight) {
    91 				expectedRight = newX;
    92 			}
    93 			if (expectedBottom === undefined || newY > expectedBottom) {
    94 				expectedBottom = newY;
    95 			}
    96 			if (expectedLeft === undefined || newX < expectedLeft) {
    97 				expectedLeft = newX;
    98 			}
    99 		}
   101 		// Don't test singular matrices for now.  IE fails some of them, which
   102 		// might be due to getBoundingClientRect() instead of transforms.
   103 		if (a*d - b*c !== 0) {
   104 			// FIXME: We assume getBoundingClientRect() returns the rectangle
   105 			// that contains the transformed box, not the untransformed box.
   106 			// This is not actually specified anywhere:
   107 			// https://www.w3.org/Bugs/Public/show_bug.cgi?id=15430
   108 			var rect = div.getBoundingClientRect();
   109 			assert_approx_equals(rect.top, expectedTop, epsilon, "top");
   110 			assert_approx_equals(rect.right, expectedRight, epsilon, "right");
   111 			assert_approx_equals(rect.bottom, expectedBottom, epsilon, "bottom");
   112 			assert_approx_equals(rect.left, expectedLeft, epsilon, "left");
   113 			assert_approx_equals(rect.width, expectedRight - expectedLeft, epsilon, "width");
   114 			assert_approx_equals(rect.height, expectedBottom - expectedTop, epsilon, "height");
   115 		}
   116 	}, "Boundaries with transform: " + transformValue + "; transform-origin: " + transformOriginValue);
   117 }
   119 // Test style="transform: matrix(*)" (4^6 = 4096 permutations)
   120 var matrixValues = [-1, 0, 1, 1.72];
   121 matrixValues.forEach(function(a) {
   122 matrixValues.forEach(function(b) {
   123 matrixValues.forEach(function(c) {
   124 matrixValues.forEach(function(d) {
   125 matrixValues.forEach(function(e) {
   126 matrixValues.forEach(function(f) {
   127 	testTransform(
   128 		"matrix(" + a + ", " + b + ", " + c + ", " + d + ", " + e + ", " + f + ")",
   129 		a, b, c, d, e, f
   130 	);
   131 });
   132 });
   133 });
   134 });
   135 });
   136 });
   138 // Test translate()/translateX()/translateY()
   139 //
   140 // TODO: Percentage translations
   141 var translates = [-53.7, -5, -1, -0.12, 0, 0.1, 1, 5, 53.7];
   142 translates.forEach(function(tx) {
   143 	testTransform(
   144 		"translateX(" + tx + "px)",
   145 		1, 0, 0, 1, tx, 0
   146 	);
   147 	// tx is poorly named, since it's used for y here.
   148 	testTransform(
   149 		"translateY(" + tx + "px)",
   150 		1, 0, 0, 1, 0, tx
   151 	);
   152 	testTransform(
   153 		"translate(" + tx + "px)",
   154 		1, 0, 0, 1, tx, 0
   155 	);
   157 	translates.forEach(function(ty) {
   158 		testTransform(
   159 			"translate(" + tx + "px, " + ty + "px)",
   160 			1, 0, 0, 1, tx, ty
   161 		);
   162 	});
   163 });
   165 // Test scale()/scaleX()/scaleY()
   166 var scales = [-2, -1, -0.12, 0, 0.12, 1, 2];
   167 scales.forEach(function(sx) {
   168 	testTransform(
   169 		"scaleX(" + sx + ")",
   170 		sx, 0, 0, 1, 0, 0
   171 	);
   172 	// sx is poorly named, since it's used for y here, then for both x and y.
   173 	testTransform(
   174 		"scaleY(" + sx + ")",
   175 		1, 0, 0, sx, 0, 0
   176 	);
   177 	testTransform(
   178 		"scale(" + sx + ")",
   179 		sx, 0, 0, sx, 0, 0
   180 	);
   182 	scales.forEach(function(sy) {
   183 		testTransform(
   184 			"scale(" + sx + ", " + sy + ")",
   185 			sx, 0, 0, sy, 0, 0
   186 		);
   187 	});
   188 });
   190 // Test rotate()
   191 var rotates = [-7, 0, 22.5, 45, 86.451, 90, 180, 270, 452];
   192 rotates.forEach(function(angle) {
   193 	testTransform(
   194 		"rotate(" + angle + "deg)",
   195 		Math.cos(angle * Math.PI/180),
   196 		Math.sin(angle * Math.PI/180),
   197 		-Math.sin(angle * Math.PI/180),
   198 		Math.cos(angle * Math.PI/180),
   199 		0,
   200 		0
   201 	);
   202 });
   204 // Test skewX()/skewY()
   205 //
   206 // Do not test values close to 90 degrees, because this will cause coordinates
   207 // to get large.  The maximum values for coordinates are (of course) not
   208 // defined, and even if they were, the result would be extremely sensitive to
   209 // rounding error.
   210 var skews = [-80, -45, -32.6, -0.05, 0, 0.05, 32.6, 45, 80, 300];
   211 skews.forEach(function(angle) {
   212 	testTransform(
   213 		"skewX(" + angle + "deg)",
   214 		1, 0, Math.tan(angle * Math.PI/180), 1, 0, 0
   215 	);
   216 	testTransform(
   217 		"skewY(" + angle + "deg)",
   218 		1, Math.tan(angle * Math.PI/180), 0, 1, 0, 0
   219 	);
   220 });
   223 /**
   224  * Test that "transform-origin: value" acts like the origin is at
   225  * (expectedHoriz, expectedVert), where the latter two parameters can be
   226  * keywords, percentages, or lengths.  Tests both that the computed value is
   227  * correct, and that the boundary box is as expected for a 45-degree rotation.
   228  *
   229  * TODO: Allow non-pixel units.
   230  */
   231 function testTransformOrigin(value, expectedHoriz, expectedVert) {
   232 	if (expectedHoriz == "left") {
   233 		expectedHoriz = "0%";
   234 	} else if (expectedHoriz == "center") {
   235 		expectedHoriz = "50%";
   236 	} else if (expectedHoriz == "right") {
   237 		expectedHoriz = "100%";
   238 	} else if (expectedHoriz == "0") {
   239 		// CSSOM says a length of zero is "0px".
   240 		expectedHoriz = "0px";
   241 	}
   242 	if (expectedVert == "top") {
   243 		expectedVert = "0%";
   244 	} else if (expectedVert == "center") {
   245 		expectedVert = "50%";
   246 	} else if (expectedVert == "bottom") {
   247 		expectedVert = "100%";
   248 	} else if (expectedVert == "0") {
   249 		expectedVert = "0px";
   250 	}
   251 	// FIXME: Nothing defines resolved values here.  I picked the behavior of
   252 	// all non-Gecko engines, which is also the behavior Gecko for transforms
   253 	// other than "none": https://www.w3.org/Bugs/Public/show_bug.cgi?id=15433
   254 	if (/%$/.test(expectedHoriz)) {
   255 		expectedHoriz = (parseFloat(expectedHoriz) * divWidth/100) + "px";
   256 	}
   257 	if (/%$/.test(expectedVert)) {
   258 		expectedVert = (parseFloat(expectedVert) * divHeight/100) + "px";
   259 	}
   261 	testTransformOriginParsing(value, expectedHoriz, expectedVert);
   263 	// Test with a 45-degree rotation, since the effect of changing the origin
   264 	// will be easy to understand.
   265 	testTransformedBoundary(
   266 		// Transform
   267 		"rotate(45deg)",
   268 		// Matrix entries
   269 		Math.cos(Math.PI/4), Math.sin(Math.PI/4),
   270 		-Math.sin(Math.PI/4), Math.cos(Math.PI/4),
   271 		0, 0,
   272 		// Origin
   273 		value, parseFloat(expectedHoriz), parseFloat(expectedVert)
   274 	);
   275 }
   277 /**
   278  * Tests that style="transform-origin: value" results in
   279  * getComputedStyle().transformOrigin being expectedHoriz + " " + expectedVert.
   280  * expectedHoriz and expectedVert are always in pixels.
   281  */
   282 function testTransformOriginParsing(value, expectedHoriz, expectedVert) {
   283 	if (testTransformOriginParsing.counter === undefined) {
   284 		testTransformOriginParsing.counter = 0;
   285 	}
   286 	// The transform doesn't matter here, so set it to one of several
   287 	// possibilities arbitrarily (this actually catches a Gecko bug!)
   288 	var transformValue = {
   289 		0: "none",
   290 		1: "matrix(7, 0, -1, 13, 0, 0)",
   291 		2: "translate(4em, -15px)",
   292 		3: "scale(1.2, 1)",
   293 		4: "rotate(43deg)",
   294 	}[testTransformOriginParsing.counter % 5];
   295 	testTransformOriginParsing.counter++;
   296 	test(function() {
   297 		// TODO: test div.setAttribute("style", ...) too
   298 		div.style[prop] = transformValue;
   299 		div.style[prop + "Origin"] = value;
   300 		var actual = getComputedStyle(div)[prop + "Origin"];
   301 		var re = /^([^ ]+)px ([^ ]+)px$/;
   302 		assert_regexp_match(actual, re, "Computed value has unexpected form");
   303 		var match = re.exec(actual);
   305 		assert_approx_equals(Number(match[1]), parseFloat(expectedHoriz),
   306 			epsilon, "Value of horizontal part (actual: "
   307 				 + actual + ", expected " + expectedHoriz + " " + expectedVert + ")");
   309 		assert_approx_equals(Number(match[2]), parseFloat(expectedVert),
   310 			epsilon, "Value of vertical part (actual: "
   311 				 + actual + ", expected " + expectedHoriz + " " + expectedVert + ")");
   312 	}, "Computed value for transform-origin with transform-origin: " + value + "; transform: " + transformValue);
   313 }
   315 // Test transform-origin with one argument
   316 [
   317 	["none", "50%", "50%"],
   318 	["quasit", "50%", "50%"],
   319 	["top", "50%", "0%"],
   320 	["right", "100%", "50%"],
   321 	["bottom", "50%", "100%"],
   322 	[" BOttOm\t", "50%", "100%"],
   323 	["left", "0%", "50%"],
   324 	["center", "50%", "50%"],
   325 	["37%", "37%", "50%"],
   326 	["117%", "117%", "50%"],
   327 	["41.2px", "41.2px", "50%"],
   328 	["-31.8px", "-31.8px", "50%"],
   329 ].forEach(function(arr) {
   330 	testTransformOrigin(arr[0], arr[1], arr[2]);
   331 });
   333 // Test transform-origin with two arguments
   334 var percentagesAndLengths = ["-12%", "0%", "50%", "51.235%", "100%", "126%",
   335 	"-15px", "-1px", "-0.25px", "0", "0px", "0.25px", "1px", "15px"];
   336 var originArguments1 = ["left", "center", "right"]
   337 	.concat(percentagesAndLengths);
   338 var originArguments2 = ["top", "center", "bottom"]
   339 	.concat(percentagesAndLengths);
   340 originArguments1.forEach(function(arg1) {
   341 	originArguments2.forEach(function(arg2) {
   342 		testTransformOrigin(arg1 + " " + arg2, arg1, arg2);
   343 	});
   344 });
   346 // FIXME: Three- and four-value variants are not generally implemented
   347 // https://www.w3.org/Bugs/Public/show_bug.cgi?id=15432
   348 /*
   349 percentagesAndLengths.forEach(function(height) {
   350 	// height is a misnomer, it's also used for width in this part.
   351 	// Three arguments with width first:
   352 	testTransformOrigin("center top " + height, "", "");
   353 	testTransformOrigin("center bottom " + height, "", "");
   355 	testTransformOrigin("left " + height + " center", "", "");
   356 	testTransformOrigin("left " + height + " top", "", "");
   357 	testTransformOrigin("left " + height + " bottom", "", "");
   358 	testTransformOrigin("left top " + height, "", "");
   359 	testTransformOrigin("left bottom " + height, "", "");
   361 	testTransformOrigin("right " + height + " center", "", "");
   362 	testTransformOrigin("right " + height + " top", "", "");
   363 	testTransformOrigin("right " + height + " bottom", "", "");
   364 	testTransformOrigin("right top " + height, "", "");
   365 	testTransformOrigin("right bottom " + height, "", "");
   367 	// Three arguments with height first:
   368 	testTransformOrigin("center left " + height, "", "");
   369 	testTransformOrigin("center right " + height, "", "");
   371 	testTransformOrigin("top " + height + " center", "", "");
   372 	testTransformOrigin("top " + height + " left", "", "");
   373 	testTransformOrigin("top " + height + " right", "", "");
   374 	testTransformOrigin("top left " + height, "", "");
   375 	testTransformOrigin("top right " + height, "", "");
   377 	testTransformOrigin("bottom " + height + " center", "", "");
   378 	testTransformOrigin("bottom " + height + " left", "", "");
   379 	testTransformOrigin("bottom " + height + " right", "", "");
   380 	testTransformOrigin("bottom left " + height, "", "");
   381 	testTransformOrigin("bottom right " + height, "", "");
   383 	// Four arguments
   384 	percentagesAndLengths.forEach(function(width) {
   385 		// Width first:
   386 		testTransformOrigin("left " + width + " top " + height, "", "");
   387 		testTransformOrigin("left " + width + " bottom " + height, "", "");
   388 		testTransformOrigin("right " + width + " top " + height, "", "");
   389 		testTransformOrigin("right " + width + " bottom " + height, "", "");
   391 		// Height first:
   392 		testTransformOrigin("top " + height + " left " + width, "", "");
   393 		testTransformOrigin("top " + height + " right " + width, "", "");
   394 		testTransformOrigin("bottom " + height + " left " + width, "", "");
   395 		testTransformOrigin("bottom " + height + " right " + width, "", "");
   396 	});
   397 });
   398 */
   400 document.querySelector("style").disabled = true;
   401 </script>

mercurial