chromium/third_party/blink/web_tests/svg/as-object/sizing/svg-in-object.js

// global async_test, assert_equals
//
// This test generates a couple of scenarios (each a TestData) for
// sizing SVG inside an inline <object> and has a simple JavaScript
// sizing implementation that handles the generated scenarios. It
// generates a DOM corresponding to the scenario and compares the laid
// out size to the calculated size.
//
// The tests loops through different combinations of:
//
// * width and height on <object>
//
// * width and height on <svg>
//
// * viewBox on <svg> (gives intrinsic ratio)
//
// * width and height on containing block of <object>
//
// All these contribute to the final size of the SVG in some way.
//
// The test focuses on the size of the CSS box generated by the SVG.
// The SVG is always empty by itself so no actual SVG are tested.
//
// Focus is also put on how the different attributes interact, little
// focus is put on variations within an attribute that doesn't affect
// the relationship to other attributes, i.e only px and % units are
// used since that covers the interactions.
//
// To debug a specific test append ?<test-id> to the URL. An <iframe>
// is generated with equivalent test and the source of the test is
// added to a <pre> element.
//
// Note: placeholder is an alternative name for the tested <object>
// element; 'object' becomes such an ambigious name when placed in
// code.

(function() {
    function parseLength(l) {
        var match = /^([-+]?[0-9]+|[-+]?[0-9]*\.[0-9]+)(px|%)?$/.exec(l);
        if (!match)
            return null;
        return new Length(Number(match[1]), match[2] ? match[2] : "px");
    }

    function parseViewBox(input) {
        if (!input)
            return null;

        var arr = input.split(' ');
        return arr.map(function(a) { return parseInt(a); });
    }

    // Only px and % are used
    function convertToPx(input, percentRef) {
        if (input == null)
            return null;
        var length = parseLength(input);
        if (length.amount == 0)
            return 0;
        if (!length.unit)
            length.unit = "px";
        if (length.unit == "%" && percentRef === undefined)
            return null;
        return length.amount * { px: 1,
                                 "%": percentRef/100}[length.unit];
    }

    function Length(amount, unit) {
        this.amount = amount;
        this.unit = unit;
    }

    function describe(data) {
        function dumpObject(obj) {
            var r = "";
            for (var property in obj) {
                if (obj.hasOwnProperty(property)) {
                    var value = obj[property];
                    if (typeof value == 'string')
                        value = "'" + value + "'";
                    else if (value == null)
                        value = "null";
                    else if (typeof value == 'object')
                    {
                        if (value instanceof Array)
                            value = "[" + value + "]";
                        else
                            value = "{" + dumpObject(value) + "}";
                    }

                    if (value != "null")
                        r += property + ": " + value + ", ";
                }
            }
            return r;
        }
        var result = dumpObject(data);
        if (result == "")
            return "(initial values)";
        return result;
    }

    function TestData(config) {
        this.config = config;
        this.name = describe(config);
        this.style = {};
        this.mapPresentationalHintLength("width", config.placeholderWidthAttr);
        this.mapPresentationalHintLength("height", config.placeholderHeightAttr);
    }

    TestData.prototype.mapPresentationalHintLength =
        function(cssProperty, attr) {
            if (attr) {
                var l = parseLength(attr);
                if (l)
                    this.style[cssProperty] = l.amount + l.unit;
            }
        };

    TestData.prototype.computedWidthIsAuto = function() {
        return !this.style["width"] || this.style["width"] == 'auto';
    };

    TestData.prototype.computedHeightIsAuto = function() {
        return !this.style["height"] || this.style["height"] == 'auto' ||
            (parseLength(this.style["height"]).unit == '%' &&
             this.containerComputedHeightIsAuto());
    };

    TestData.prototype.containerComputedWidthIsAuto = function() {
        return !this.config.containerWidthStyle ||
            this.config.containerWidthStyle == 'auto';
    };

    TestData.prototype.containerComputedHeightIsAuto = function() {
        return !this.config.containerHeightStyle ||
            this.config.containerHeightStyle == 'auto';
    };

    TestData.prototype.intrinsicInformation = function() {
        var w = convertToPx(this.config.svgWidthAttr) || 0;
        var h = convertToPx(this.config.svgHeightAttr) || 0;
        var r = 0;
        if (w && h) {
            r =  w / h;
        } else {
            var vb = parseViewBox(this.config.svgViewBoxAttr);
            if (vb) {
                r = vb[2] / vb[3];
            }
            if (r) {
                if (!w && h)
                    w = h * r;
                else if (!h && w)
                    h = w / r;
            }
        }
        return { width: w, height: h, ratio: r };
    };


    TestData.prototype.computeInlineReplacedSize = function() {
        var intrinsic = this.intrinsicInformation();
        var self = this;

        // http://www.w3.org/TR/CSS2/visudet.html#inline-replaced-height
        function calculateUsedHeight() {
            if (self.computedHeightIsAuto()) {
                if (self.computedWidthIsAuto() && intrinsic.height)
                    return intrinsic.height;
                if (intrinsic.ratio)
                    return calculateUsedWidth() / intrinsic.ratio;
                if (intrinsic.height)
                    return intrinsic.height;
                return 150;
            }

            return convertToPx(self.style["height"],
                               convertToPx(self.config.containerHeightStyle,
                                           self.outerHeight));
        }

        // http://www.w3.org/TR/CSS2/visudet.html#inline-replaced-width
        function calculateUsedWidth() {
            if (self.computedWidthIsAuto()) {
                if (self.computedHeightIsAuto() && intrinsic.width)
                    return intrinsic.width;
                if (!self.computedHeightIsAuto() && intrinsic.ratio)
                    return calculateUsedHeight() * intrinsic.ratio;
                if (self.computedHeightIsAuto() && intrinsic.ratio) {
                    if (self.containerComputedWidthIsAuto()) {
                        // Note: While this is actually undefined in CSS
                        // 2.1, use the suggested value by examining the
                        // ancestor widths.
                        return self.outerWidth;
                    } else {
                        return convertToPx(self.config.containerWidthStyle,
                                           self.outerWidth);
                    }
                }
                if (intrinsic.width)
                    return intrinsic.width;
                return 300;
            }

            if (self.containerComputedWidthIsAuto())
                return convertToPx(self.style["width"], self.outerWidth);
            else
                return convertToPx(self.style["width"],
                                   convertToPx(self.config.containerWidthStyle,
                                               self.outerWidth));
        }
        return { width: calculateUsedWidth(),
                 height: calculateUsedHeight() };
    };

    var testContainer = document.querySelector('#testContainer');
    TestData.prototype.outerWidth = testContainer.getBoundingClientRect().width;
    TestData.prototype.outerHeight = testContainer.getBoundingClientRect().height;

    window.TestData = TestData;
})();

function setupContainer(testData, placeholder, options) {
    options = options || {};

    var container = document.createElement("div");

    container.id = "container";
    if (testData.config.containerWidthStyle)
        container.style.width = testData.config.containerWidthStyle;

    if (testData.config.containerHeightStyle)
        container.style.height = testData.config.containerHeightStyle;

    if (options.pretty)
        container.appendChild(document.createTextNode("\n\t\t"));
    container.appendChild(placeholder);
    if (options.pretty)
        container.appendChild(document.createTextNode("\n\t"));

    return container;
}

function setupPlaceholder(testData, options) {
    options = options || {};

    function generateSVGURI(testData, encoder) {
        var res = '<svg xmlns="http://www.w3.org/2000/svg"';
        function addAttr(attr, prop) {
            if (testData.config[prop])
                res += ' ' + attr + '="' + testData.config[prop] + '"';
        }
        addAttr("width", "svgWidthAttr");
        addAttr("height", "svgHeightAttr");
        addAttr("viewBox", "svgViewBoxAttr");
        res += '></svg>';
        return 'data:image/svg+xml' + encoder(res);
    }

    var placeholder = document.createElement("object");

    if (options.pretty) {
        placeholder.appendChild(document.createTextNode("\n\t\t\t"));
        placeholder.appendChild(
            document.createComment(
                generateSVGURI(testData, function(x) { return "," + x; })));
        placeholder.appendChild(document.createTextNode("\n\t\t"));
    }

    placeholder.setAttribute("id", "test");
    if (testData.config.placeholderWidthAttr)
        placeholder.setAttribute("width", testData.config.placeholderWidthAttr);
    if (testData.config.placeholderHeightAttr)
        placeholder.setAttribute("height", testData.config.placeholderHeightAttr);
    placeholder.setAttribute("data",
                             generateSVGURI(testData, function(x) {
                                 return ";base64," + btoa(x);
                             }));
    return placeholder;
}

function buildDemo(testData) {
    // Non-essential debugging tool

    var options = { pretty: true };
    var expectedRect =
            testData.computeInlineReplacedSize();
    var container =
            setupContainer(testData, setupPlaceholder(testData, options), options);

    var root = document.createElement("html");
    var style = document.createElement("style");

    style.textContent = "\n" +
        "\tbody { margin: 0; font-family: sans-serif }\n" +
        "\t#expected {\n" +
        "\t\twidth: " + (expectedRect.width) + "px; height: "
        + (expectedRect.height) + "px;\n" +
        "\t\tborder: 10px solid lime; position: absolute;\n" +
        "\t\tbackground-color: red }\n" +
        "\t#testContainer { position: absolute;\n" +
        "\t\ttop: 10px; left: 10px; width: 800px; height: 600px }\n" +
        "\tobject { background-color: green }\n" +
        "\t.result { position: absolute; top: 0; right: 0;\n" +
        "\t\tbackground-color: hsla(0,0%, 0%, 0.85); border-radius: 0.5em;\n" +
        "\t\tpadding: 0.5em; border: 0.25em solid black }\n" +
        "\t.pass { color: lime }\n" +
        "\t.fail { color: red }\n";

    root.appendChild(document.createTextNode("\n"));
    root.appendChild(style);
    root.appendChild(document.createTextNode("\n"));

    var script = document.createElement("script");
    script.textContent = "\n" +
        "onload = function() {\n" +
        "\tvar objectRect = \n" +
        "\t\tdocument.querySelector('#test').getBoundingClientRect();\n" +
        "\tpassed = (objectRect.width == " + expectedRect.width + " && " +
        "objectRect.height == " + expectedRect.height + ");\n" +
        "\tdocument.body.insertAdjacentHTML('beforeEnd',\n" +
        "\t\t'<span class=\"result '+ (passed ? 'pass' : 'fail') " +
        "+ '\">' + (passed ? 'Pass' : 'Fail') + '</span>');\n" +
        "};\n";

    root.appendChild(script);
    root.appendChild(document.createTextNode("\n"));

    var expectedElement = document.createElement("div");
    expectedElement.id = "expected";
    root.appendChild(expectedElement);
    root.appendChild(document.createTextNode("\n"));

    var testContainer = document.createElement("div");
    testContainer.id = "testContainer";
    testContainer.appendChild(document.createTextNode("\n\t"));
    testContainer.appendChild(container);
    testContainer.appendChild(document.createTextNode("\n"));
    root.appendChild(testContainer);
    root.appendChild(document.createTextNode("\n"));

    return "<!DOCTYPE html>\n" + root.outerHTML;
}

function doCombinationTest(values, func)
{
    // Recursively construct all possible combinations of values and
    // send them to |func|. Example:
    //
    // values: [["X", ["a", "b"]],
    //          ["Y", ["c", "d"]]]
    //
    // generates the objects:
    //
    // 1: { "X": "a", "Y": "c" }
    // 2: { "X": "a", "Y": "d" }
    // 3: { "X": "b", "Y": "c" }
    // 4: { "X": "b", "Y": "d" }
    //
    // and each will be sent to |func| with the corresponding prefixed
    // id (modulo order).

    var combinationId = 1;
    function doCombinationTestRecursive(slicedValues, config) {
        if (slicedValues.length > 0) {
            var configKey = slicedValues[0][0];
            slicedValues[0][1].forEach(function(configValue) {
                var new_config = {};
                for (k in config)
                    new_config[k] = config[k];
                new_config[configKey] = configValue;
                doCombinationTestRecursive(slicedValues.slice(1), new_config);
            });
        } else {
            func(config, combinationId++);
        }
    }
    doCombinationTestRecursive(values, {});
}

var debugHint = function(id) { return "(append ?"+id+" to debug) " };
var testSingleId;
if (window.location.search) {
    testSingleId = window.location.search.substring(1);
    debugHint = function(id) { return ""; };
}

function testSVGInObjectWithPlaceholder(placeholderWidthAttr, placeholderHeightAttr, viewBoxAttr) {
    doCombinationTest(
        [["containerWidthStyle", [null, "400px"]],
         ["containerHeightStyle", [null, "400px"]],
         ["placeholderWidthAttr", [placeholderWidthAttr]],
         ["placeholderHeightAttr", [placeholderHeightAttr]],
         ["svgViewBoxAttr", [ viewBoxAttr ]],
         ["svgWidthAttr", [ null, "200", "25%" ]],
         ["svgHeightAttr", [ null, "200", "25%" ]]],
        function(config, id) {
            if (!testSingleId || testSingleId == id) {
                var testData = new TestData(config);
                var t = async_test(testData.name);

                var expectedRect =
                        testData.computeInlineReplacedSize();
                var placeholder = setupPlaceholder(testData);
                var container =
                        setupContainer(testData, placeholder);

                var checkSize = function() {
                    var placeholderRect =
                            placeholder.getBoundingClientRect();

                    try {
                        assert_approx_equals(placeholderRect.width,
                                             expectedRect.width,
                                             0.5,
                                             debugHint(id) + "Wrong width");
                        assert_approx_equals(placeholderRect.height,
                                             expectedRect.height,
                                             0.5,
                                             debugHint(id) + "Wrong height");
                    } finally {
                        testContainer.removeChild(container);
                        if (testSingleId)
                            document.body.removeChild(testContainer);
                    }
                    t.done();
                };

                t.step(function() {
                    placeholder.addEventListener('load', function() {
                        // setTimeout is a work-around to let engines
                        // finish layout of child browsing contexts even
                        // after the load event
                        setTimeout(t.step_func(checkSize), 0);
                    });
                    testContainer.appendChild(container);
                });
            }

            if (testSingleId == id) {
                var pad = function(n, width, z) {
                    z = z || '0';
                    n = n + '';
                    return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n;
                };

                var demo = buildDemo(testData);
                var iframe = document.createElement('iframe');
                iframe.style.width = (Math.max(900, expectedRect.width)) + "px";
                iframe.style.height = (Math.max(400, expectedRect.height)) + "px";
                iframe.src = "data:text/html;charset=utf-8," + encodeURIComponent(demo);
                document.body.appendChild(iframe);

                document.body.insertAdjacentHTML(
                    'beforeEnd',
                    '<p><a href="data:application/octet-stream;charset=utf-8;base64,' +
                        btoa(demo) + '" download="svg-in-object-test-' + pad(id, 3) + '.html">Download</a></p>');
            }
        });
}