OLD | NEW |
(Empty) | |
| 1 // global async_test, assert_equals |
| 2 // |
| 3 // This test generates a couple of scenarios (each a TestData) for |
| 4 // sizing SVG inside an inline <object> and has a simple JavaScript |
| 5 // sizing implementation that handles the generated scenarios. It |
| 6 // generates a DOM corresponding to the scenario and compares the laid |
| 7 // out size to the calculated size. |
| 8 // |
| 9 // The tests loops through different combinations of: |
| 10 // |
| 11 // * width and height on <object> |
| 12 // |
| 13 // * width and height on <svg> |
| 14 // |
| 15 // * viewBox on <svg> (gives intrinsic ratio) |
| 16 // |
| 17 // * width and height on containing block of <object> |
| 18 // |
| 19 // All these contribute to the final size of the SVG in some way. |
| 20 // |
| 21 // The test focuses on the size of the CSS box generated by the SVG. |
| 22 // The SVG is always empty by itself so no actual SVG are tested. |
| 23 // |
| 24 // Focus is also put on how the different attributes interact, little |
| 25 // focus is put on variations within an attribute that doesn't affect |
| 26 // the relationship to other attributes, i.e only px and % units are |
| 27 // used since that covers the interactions. |
| 28 // |
| 29 // To debug a specific test append ?<test-id> to the URL. An <iframe> |
| 30 // is generated with equivalent test and the source of the test is |
| 31 // added to a <pre> element. |
| 32 // |
| 33 // Note: placeholder is an alternative name for the tested <object> |
| 34 // element; 'object' becomes such an ambigious name when placed in |
| 35 // code. |
| 36 |
| 37 (function() { |
| 38 function parseLength(l) { |
| 39 var match = /^([-+]?[0-9]+|[-+]?[0-9]*\.[0-9]+)(px|%)?$/.exec(l); |
| 40 if (!match) |
| 41 return null; |
| 42 return new Length(Number(match[1]), match[2] ? match[2] : "px"); |
| 43 } |
| 44 |
| 45 function parseViewBox(input) { |
| 46 if (!input) |
| 47 return null; |
| 48 |
| 49 var arr = input.split(' '); |
| 50 return arr.map(function(a) { return parseInt(a); }); |
| 51 } |
| 52 |
| 53 // Only px and % are used |
| 54 function convertToPx(input, percentRef) { |
| 55 if (input == null) |
| 56 return null; |
| 57 var length = parseLength(input); |
| 58 if (length.amount == 0) |
| 59 return 0; |
| 60 if (!length.unit) |
| 61 length.unit = "px"; |
| 62 if (length.unit == "%" && percentRef === undefined) |
| 63 return null; |
| 64 return length.amount * { px: 1, |
| 65 "%": percentRef/100}[length.unit]; |
| 66 } |
| 67 |
| 68 function Length(amount, unit) { |
| 69 this.amount = amount; |
| 70 this.unit = unit; |
| 71 } |
| 72 |
| 73 function describe(data) { |
| 74 function dumpObject(obj) { |
| 75 var r = ""; |
| 76 for (var property in obj) { |
| 77 if (obj.hasOwnProperty(property)) { |
| 78 var value = obj[property]; |
| 79 if (typeof value == 'string') |
| 80 value = "'" + value + "'"; |
| 81 else if (value == null) |
| 82 value = "null"; |
| 83 else if (typeof value == 'object') |
| 84 { |
| 85 if (value instanceof Array) |
| 86 value = "[" + value + "]"; |
| 87 else |
| 88 value = "{" + dumpObject(value) + "}"; |
| 89 } |
| 90 |
| 91 if (value != "null") |
| 92 r += property + ": " + value + ", "; |
| 93 } |
| 94 } |
| 95 return r; |
| 96 } |
| 97 var result = dumpObject(data); |
| 98 if (result == "") |
| 99 return "(initial values)"; |
| 100 return result; |
| 101 } |
| 102 |
| 103 function TestData(config) { |
| 104 this.config = config; |
| 105 this.name = describe(config); |
| 106 this.style = {}; |
| 107 this.mapPresentationalHintLength("width", config.placeholderWidthAttr); |
| 108 this.mapPresentationalHintLength("height", config.placeholderHeightAttr)
; |
| 109 } |
| 110 |
| 111 TestData.prototype.mapPresentationalHintLength = |
| 112 function(cssProperty, attr) { |
| 113 if (attr) { |
| 114 var l = parseLength(attr); |
| 115 if (l) |
| 116 this.style[cssProperty] = l.amount + l.unit; |
| 117 } |
| 118 }; |
| 119 |
| 120 TestData.prototype.computedWidthIsAuto = function() { |
| 121 return !this.style["width"] || this.style["width"] == 'auto'; |
| 122 }; |
| 123 |
| 124 TestData.prototype.computedHeightIsAuto = function() { |
| 125 return !this.style["height"] || this.style["height"] == 'auto' || |
| 126 (parseLength(this.style["height"]).unit == '%' && |
| 127 this.containerComputedHeightIsAuto()); |
| 128 }; |
| 129 |
| 130 TestData.prototype.containerComputedWidthIsAuto = function() { |
| 131 return !this.config.containerWidthStyle || |
| 132 this.config.containerWidthStyle == 'auto'; |
| 133 }; |
| 134 |
| 135 TestData.prototype.containerComputedHeightIsAuto = function() { |
| 136 return !this.config.containerHeightStyle || |
| 137 this.config.containerHeightStyle == 'auto'; |
| 138 }; |
| 139 |
| 140 TestData.prototype.intrinsicInformation = function() { |
| 141 var w = convertToPx(this.config.svgWidthAttr) || 0; |
| 142 var h = convertToPx(this.config.svgHeightAttr) || 0; |
| 143 var r = 0; |
| 144 if (w && h) { |
| 145 r = w / h; |
| 146 } else { |
| 147 var vb = parseViewBox(this.config.svgViewBoxAttr); |
| 148 if (vb) { |
| 149 r = vb[2] / vb[3]; |
| 150 } |
| 151 if (r) { |
| 152 if (!w && h) |
| 153 w = h * r; |
| 154 else if (!h && w) |
| 155 h = w / r; |
| 156 } |
| 157 } |
| 158 return { width: w, height: h, ratio: r }; |
| 159 }; |
| 160 |
| 161 |
| 162 TestData.prototype.computeInlineReplacedSize = function() { |
| 163 var intrinsic = this.intrinsicInformation(); |
| 164 var self = this; |
| 165 |
| 166 // http://www.w3.org/TR/CSS2/visudet.html#inline-replaced-height |
| 167 function calculateUsedHeight() { |
| 168 if (self.computedHeightIsAuto()) { |
| 169 if (self.computedWidthIsAuto() && intrinsic.height) |
| 170 return intrinsic.height; |
| 171 if (intrinsic.ratio) |
| 172 return calculateUsedWidth() / intrinsic.ratio; |
| 173 if (intrinsic.height) |
| 174 return intrinsic.height; |
| 175 return 150; |
| 176 } |
| 177 |
| 178 return convertToPx(self.style["height"], |
| 179 convertToPx(self.config.containerHeightStyle, |
| 180 self.outerHeight)); |
| 181 } |
| 182 |
| 183 // http://www.w3.org/TR/CSS2/visudet.html#inline-replaced-width |
| 184 function calculateUsedWidth() { |
| 185 if (self.computedWidthIsAuto()) { |
| 186 if (self.computedHeightIsAuto() && intrinsic.width) |
| 187 return intrinsic.width; |
| 188 if (!self.computedHeightIsAuto() && intrinsic.ratio) |
| 189 return calculateUsedHeight() * intrinsic.ratio; |
| 190 if (self.computedHeightIsAuto() && intrinsic.ratio) { |
| 191 if (self.containerComputedWidthIsAuto()) { |
| 192 // Note: While this is actually undefined in CSS |
| 193 // 2.1, use the suggested value by examining the |
| 194 // ancestor widths. |
| 195 return self.outerWidth; |
| 196 } else { |
| 197 return convertToPx(self.config.containerWidthStyle, |
| 198 self.outerWidth); |
| 199 } |
| 200 } |
| 201 if (intrinsic.width) |
| 202 return intrinsic.width; |
| 203 return 300; |
| 204 } |
| 205 |
| 206 if (self.containerComputedWidthIsAuto()) |
| 207 return convertToPx(self.style["width"], self.outerWidth); |
| 208 else |
| 209 return convertToPx(self.style["width"], |
| 210 convertToPx(self.config.containerWidthStyle, |
| 211 self.outerWidth)); |
| 212 } |
| 213 return { width: calculateUsedWidth(), |
| 214 height: calculateUsedHeight() }; |
| 215 }; |
| 216 |
| 217 var testContainer = document.querySelector('#testContainer'); |
| 218 TestData.prototype.outerWidth = testContainer.getBoundingClientRect().width; |
| 219 TestData.prototype.outerHeight = testContainer.getBoundingClientRect().heigh
t; |
| 220 |
| 221 window.TestData = TestData; |
| 222 })(); |
| 223 |
| 224 function setupContainer(testData, placeholder, options) { |
| 225 options = options || {}; |
| 226 |
| 227 var container = document.createElement("div"); |
| 228 |
| 229 container.id = "container"; |
| 230 if (testData.config.containerWidthStyle) |
| 231 container.style.width = testData.config.containerWidthStyle; |
| 232 |
| 233 if (testData.config.containerHeightStyle) |
| 234 container.style.height = testData.config.containerHeightStyle; |
| 235 |
| 236 if (options.pretty) |
| 237 container.appendChild(document.createTextNode("\n\t\t")); |
| 238 container.appendChild(placeholder); |
| 239 if (options.pretty) |
| 240 container.appendChild(document.createTextNode("\n\t")); |
| 241 |
| 242 return container; |
| 243 } |
| 244 |
| 245 function setupPlaceholder(testData, options) { |
| 246 options = options || {}; |
| 247 |
| 248 function generateSVGURI(testData, encoder) { |
| 249 var res = '<svg xmlns="http://www.w3.org/2000/svg"'; |
| 250 function addAttr(attr, prop) { |
| 251 if (testData.config[prop]) |
| 252 res += ' ' + attr + '="' + testData.config[prop] + '"'; |
| 253 } |
| 254 addAttr("width", "svgWidthAttr"); |
| 255 addAttr("height", "svgHeightAttr"); |
| 256 addAttr("viewBox", "svgViewBoxAttr"); |
| 257 res += '></svg>'; |
| 258 return 'data:image/svg+xml' + encoder(res); |
| 259 } |
| 260 |
| 261 var placeholder = document.createElement("object"); |
| 262 |
| 263 if (options.pretty) { |
| 264 placeholder.appendChild(document.createTextNode("\n\t\t\t")); |
| 265 placeholder.appendChild( |
| 266 document.createComment( |
| 267 generateSVGURI(testData, function(x) { return "," + x; }))); |
| 268 placeholder.appendChild(document.createTextNode("\n\t\t")); |
| 269 } |
| 270 |
| 271 placeholder.setAttribute("id", "test"); |
| 272 if (testData.config.placeholderWidthAttr) |
| 273 placeholder.setAttribute("width", testData.config.placeholderWidthAttr); |
| 274 if (testData.config.placeholderHeightAttr) |
| 275 placeholder.setAttribute("height", testData.config.placeholderHeightAttr
); |
| 276 placeholder.setAttribute("data", |
| 277 generateSVGURI(testData, function(x) { |
| 278 return ";base64," + btoa(x); |
| 279 })); |
| 280 return placeholder; |
| 281 } |
| 282 |
| 283 function buildDemo(testData) { |
| 284 // Non-essential debugging tool |
| 285 |
| 286 var options = { pretty: true }; |
| 287 var expectedRect = |
| 288 testData.computeInlineReplacedSize(); |
| 289 var container = |
| 290 setupContainer(testData, setupPlaceholder(testData, options), option
s); |
| 291 |
| 292 var root = document.createElement("html"); |
| 293 var style = document.createElement("style"); |
| 294 |
| 295 style.textContent = "\n" + |
| 296 "\tbody { margin: 0; font-family: sans-serif }\n" + |
| 297 "\t#expected {\n" + |
| 298 "\t\twidth: " + (expectedRect.width) + "px; height: " |
| 299 + (expectedRect.height) + "px;\n" + |
| 300 "\t\tborder: 10px solid lime; position: absolute;\n" + |
| 301 "\t\tbackground-color: red }\n" + |
| 302 "\t#testContainer { position: absolute;\n" + |
| 303 "\t\ttop: 10px; left: 10px; width: 800px; height: 600px }\n" + |
| 304 "\tobject { background-color: green }\n" + |
| 305 "\t.result { position: absolute; top: 0; right: 0;\n" + |
| 306 "\t\tbackground-color: hsla(0,0%, 0%, 0.85); border-radius: 0.5em;\n" + |
| 307 "\t\tpadding: 0.5em; border: 0.25em solid black }\n" + |
| 308 "\t.pass { color: lime }\n" + |
| 309 "\t.fail { color: red }\n"; |
| 310 |
| 311 root.appendChild(document.createTextNode("\n")); |
| 312 root.appendChild(style); |
| 313 root.appendChild(document.createTextNode("\n")); |
| 314 |
| 315 var script = document.createElement("script"); |
| 316 script.textContent = "\n" + |
| 317 "onload = function() {\n" + |
| 318 "\tvar objectRect = \n" + |
| 319 "\t\tdocument.querySelector('#test').getBoundingClientRect();\n" + |
| 320 "\tpassed = (objectRect.width == " + expectedRect.width + " && " + |
| 321 "objectRect.height == " + expectedRect.height + ");\n" + |
| 322 "\tdocument.body.insertAdjacentHTML('beforeEnd',\n" + |
| 323 "\t\t'<span class=\"result '+ (passed ? 'pass' : 'fail') " + |
| 324 "+ '\">' + (passed ? 'Pass' : 'Fail') + '</span>');\n" + |
| 325 "};\n"; |
| 326 |
| 327 root.appendChild(script); |
| 328 root.appendChild(document.createTextNode("\n")); |
| 329 |
| 330 var expectedElement = document.createElement("div"); |
| 331 expectedElement.id = "expected"; |
| 332 root.appendChild(expectedElement); |
| 333 root.appendChild(document.createTextNode("\n")); |
| 334 |
| 335 var testContainer = document.createElement("div"); |
| 336 testContainer.id = "testContainer"; |
| 337 testContainer.appendChild(document.createTextNode("\n\t")); |
| 338 testContainer.appendChild(container); |
| 339 testContainer.appendChild(document.createTextNode("\n")); |
| 340 root.appendChild(testContainer); |
| 341 root.appendChild(document.createTextNode("\n")); |
| 342 |
| 343 return "<!DOCTYPE html>\n" + root.outerHTML; |
| 344 } |
| 345 |
| 346 function doCombinationTest(values, func) |
| 347 { |
| 348 // Recursively construct all possible combinations of values and |
| 349 // send them to |func|. Example: |
| 350 // |
| 351 // values: [["X", ["a", "b"]], |
| 352 // ["Y", ["c", "d"]]] |
| 353 // |
| 354 // generates the objects: |
| 355 // |
| 356 // 1: { "X": "a", "Y": "c" } |
| 357 // 2: { "X": "a", "Y": "d" } |
| 358 // 3: { "X": "b", "Y": "c" } |
| 359 // 4: { "X": "b", "Y": "d" } |
| 360 // |
| 361 // and each will be sent to |func| with the corresponding prefixed |
| 362 // id (modulo order). |
| 363 |
| 364 var combinationId = 1; |
| 365 function doCombinationTestRecursive(slicedValues, config) { |
| 366 if (slicedValues.length > 0) { |
| 367 var configKey = slicedValues[0][0]; |
| 368 slicedValues[0][1].forEach(function(configValue) { |
| 369 var new_config = {}; |
| 370 for (k in config) |
| 371 new_config[k] = config[k]; |
| 372 new_config[configKey] = configValue; |
| 373 doCombinationTestRecursive(slicedValues.slice(1), new_config); |
| 374 }); |
| 375 } else { |
| 376 func(config, combinationId++); |
| 377 } |
| 378 } |
| 379 doCombinationTestRecursive(values, {}); |
| 380 } |
| 381 |
| 382 var debugHint = function(id) { return "(append ?"+id+" to debug) " }; |
| 383 var testSingleId; |
| 384 if (window.location.search) { |
| 385 testSingleId = window.location.search.substring(1); |
| 386 debugHint = function(id) { return ""; }; |
| 387 } |
| 388 |
| 389 function testSVGInObjectWithPlaceholderHeightAttr(placeholderHeightAttr) { |
| 390 // Separated over placeholderHeightAttr so that the test count is around ~20
0 |
| 391 |
| 392 doCombinationTest( |
| 393 [["containerWidthStyle", [null, "400px"]], |
| 394 ["containerHeightStyle", [null, "400px"]], |
| 395 ["placeholderWidthAttr", [null, "100", "50%"]], |
| 396 ["placeholderHeightAttr", [placeholderHeightAttr]], |
| 397 ["svgViewBoxAttr", [ null, "0 0 100 200" ]], |
| 398 ["svgWidthAttr", [ null, "200", "25%" ]], |
| 399 ["svgHeightAttr", [ null, "200", "25%" ]]], |
| 400 function(config, id) { |
| 401 if (!testSingleId || testSingleId == id) { |
| 402 var testData = new TestData(config); |
| 403 var t = async_test(testData.name); |
| 404 |
| 405 var expectedRect = |
| 406 testData.computeInlineReplacedSize(); |
| 407 var placeholder = setupPlaceholder(testData); |
| 408 var container = |
| 409 setupContainer(testData, placeholder); |
| 410 |
| 411 var checkSize = function() { |
| 412 var placeholderRect = |
| 413 placeholder.getBoundingClientRect(); |
| 414 |
| 415 try { |
| 416 assert_equals(placeholderRect.width, |
| 417 expectedRect.width, |
| 418 debugHint(id) + "Wrong width"); |
| 419 assert_equals(placeholderRect.height, |
| 420 expectedRect.height, |
| 421 debugHint(id) + "Wrong height"); |
| 422 } finally { |
| 423 testContainer.removeChild(container); |
| 424 if (testSingleId) |
| 425 document.body.removeChild(testContainer); |
| 426 } |
| 427 t.done(); |
| 428 }; |
| 429 |
| 430 t.step(function() { |
| 431 placeholder.addEventListener('load', function() { |
| 432 // setTimeout is a work-around to let engines |
| 433 // finish layout of child browsing contexts even |
| 434 // after the load event |
| 435 setTimeout(t.step_func(checkSize), 0); |
| 436 }); |
| 437 testContainer.appendChild(container); |
| 438 }); |
| 439 } |
| 440 |
| 441 if (testSingleId == id) { |
| 442 var pad = function(n, width, z) { |
| 443 z = z || '0'; |
| 444 n = n + ''; |
| 445 return n.length >= width ? n : new Array(width - n.length +
1).join(z) + n; |
| 446 }; |
| 447 |
| 448 var demo = buildDemo(testData); |
| 449 var iframe = document.createElement('iframe'); |
| 450 iframe.style.width = (Math.max(900, expectedRect.width)) + "px"; |
| 451 iframe.style.height = (Math.max(400, expectedRect.height)) + "px
"; |
| 452 iframe.src = "data:text/html;charset=utf-8," + encodeURIComponen
t(demo); |
| 453 document.body.appendChild(iframe); |
| 454 |
| 455 document.body.insertAdjacentHTML( |
| 456 'beforeEnd', |
| 457 '<p><a href="data:application/octet-stream;charset=utf-8;bas
e64,' + |
| 458 btoa(demo) + '" download="svg-in-object-test-' + pad(id,
3) + '.html">Download</a></p>'); |
| 459 } |
| 460 }); |
| 461 } |
OLD | NEW |