<!DOCTYPE HTML> <html> <!-- --> <head> <title>Test for parsing, storage, and serialization of CSS values</title> <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> <script type="text/javascript" src="property_database.js"></script> <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> <style type="text/css" id="prereqsheet"> #testnode {} </style> </head> <body> <p id="display"></p> <div id="content" style="display: none"> <div id="testnode"></div> </div> <pre id="test"> <script class="testbody" type="text/javascript"> /** Test for parsing, storage, and serialization of CSS values **/ /* * The idempotence tests here deserve a little bit of explanation. What * we're testing here are the following operations: * parse: string -> CSS rule * serialize: CSS rule -> string (normalization 1) * (this actually has two variants that go through partly different * codepaths, which we exercise with getPropertyValue and cssText) * compute: CSS rule -> computed style * cserialize: computed style -> string (normalization 2) * * Both serialize and cserialize do some normalization, so we can't test * for pure round-tripping, and we also can't compare their output since * they could normalize differently. (We might at some point in the * future want to guarantee that any output of cserialize is * untouched by going through parse+serialize, though.) * * So we test idempotence of parse + serialize by running the whole * operation twice. Likewise for parse + compute + cserialize. * * Slightly more interestingly, we test that serialize + parse is the * identity transform by comparing the output of parse + compute + * cserialize to the output of parse + serialize + parse + compute + * cserialize. */ var gSystemFont = { "caption": true, "icon": true, "menu": true, "message-box": true, "small-caption": true, "status-bar": true, "-moz-window": true, "-moz-document": true, "-moz-desktop": true, "-moz-info": true, "-moz-dialog": true, "-moz-button": true, "-moz-pull-down-menu": true, "-moz-list": true, "-moz-field": true, "-moz-workspace": true, }; var gBadCompute = { // output wrapped around to positive, in exponential notation "-moz-box-ordinal-group": [ "-1", "-1000" ], }; function xfail_compute(property, value) { if (property in gBadCompute && gBadCompute[property].indexOf(value) != -1) return true; return false; } // constructed to map longhands ==> list of containing shorthands var gPropertyShorthands = {}; var gElement = document.getElementById("testnode"); var gDeclaration = gElement.style; var gComputedStyle = window.getComputedStyle(gElement, ""); var gPrereqDeclaration = document.getElementById("prereqsheet").sheet.cssRules[0].style; // On Android, avoid most 'TEST-PASS' logging by overriding // SimpleTest.is/isnot, to improve performance if (navigator.appVersion.indexOf("Android") >= 0) { is = function is(a, b, name) { var pass = Object.is(a, b); if (!pass) SimpleTest.is(a, b, name); } isnot = function isnot(a, b, name) { var pass = !Object.is(a, b); if (!pass) SimpleTest.isnot(a, b, name); } } // Returns true if propA and propB are equivalent, considering aliasing. // (i.e. if one is an alias of the other, or if they're both aliases of // the same 3rd property) function are_properties_aliased(propA, propB) { // If either property is an alias, replace it with the property it aliases. if ("alias_for" in gCSSProperties[propA]) { propA = gCSSProperties[propA].alias_for; } if ("alias_for" in gCSSProperties[propB]) { propB = gCSSProperties[propB].alias_for; } return propA == propB; } function test_property(property) { var info = gCSSProperties[property]; // can all properties be removed from the style? function test_remove_all_properties(property, value) { var i, p = []; for (i = 0; i < gDeclaration.length; i++) p.push(gDeclaration[i]); for (i = 0; i < p.length; i++) gDeclaration.removeProperty(p[i]); var errstr = "when setting property " + property + " to " + value; is(gDeclaration.length, 0, "unremovable properties " + errstr); is(gDeclaration.cssText, "", "non-empty serialization after removing all properties " + errstr); } function test_other_shorthands_empty(value, subprop) { if (!(subprop in gPropertyShorthands)) return; var shorthands = gPropertyShorthands[subprop]; for (idx in shorthands) { var sh = shorthands[idx]; if (are_properties_aliased(sh, property)) { continue; } is(gDeclaration.getPropertyValue(sh), "", "setting '" + value + "' on '" + property + "' (for shorthand '" + sh + "')"); } } function test_value(value, resolved_value) { var value_has_variable_reference = resolved_value != null; gDeclaration.setProperty(property, value, ""); var idx; var step1val = gDeclaration.getPropertyValue(property); var step1vals = []; var step1ser = gDeclaration.cssText; if ("subproperties" in info) for (idx in info.subproperties) step1vals.push(gDeclaration.getPropertyValue(info.subproperties[idx])); var step1comp; var step1comps = []; if (info.type != CSS_TYPE_TRUE_SHORTHAND) step1comp = gComputedStyle.getPropertyValue(property); if ("subproperties" in info) for (idx in info.subproperties) step1comps.push(gComputedStyle.getPropertyValue(info.subproperties[idx])); SimpleTest.isnot(step1val, "", "setting '" + value + "' on '" + property + "'"); if ("subproperties" in info) for (idx in info.subproperties) { var subprop = info.subproperties[idx]; if (value_has_variable_reference && (!info.alias_for || info.type == CSS_TYPE_TRUE_SHORTHAND)) { is(gDeclaration.getPropertyValue(subprop), "", "setting '" + value + "' on '" + property + "' (for '" + subprop + "')"); test_other_shorthands_empty(value, subprop); } else { isnot(gDeclaration.getPropertyValue(subprop), "", "setting '" + value + "' on '" + property + "' (for '" + subprop + "')"); } } // We don't care particularly about the whitespace or the placement of // semicolons, but for simplicity we'll test the current behavior. var expected_serialization = ""; if (step1val != "") { if ("alias_for" in info) { expected_serialization = info.alias_for + ": " + step1val + ";"; } else { expected_serialization = property + ": " + step1val + ";"; } } is(step1ser, expected_serialization, "serialization should match property value"); gDeclaration.removeProperty(property); gDeclaration.setProperty(property, step1val, ""); is(gDeclaration.getPropertyValue(property), step1val, "parse+serialize should be idempotent for '" + property + ": " + value + "'"); if (info.type != CSS_TYPE_TRUE_SHORTHAND) { is(gComputedStyle.getPropertyValue(property), step1comp, "serialize+parse should be identity transform for '" + property + ": " + value + "'"); } if ("subproperties" in info && // Using setProperty over subproperties is not sufficient for // system fonts, since the shorthand does more than its parts. (property != "font" || !(value in gSystemFont)) && // Likewise for special compatibility values of transform (property != "-moz-transform" || !value.match(/^matrix.*(px|em|%)/)) && !value_has_variable_reference) { gDeclaration.removeProperty(property); for (idx in info.subproperties) { var subprop = info.subproperties[idx]; gDeclaration.setProperty(subprop, step1vals[idx], ""); } // Now that all the subprops are set, check their values. Note that we // need this in a separate loop, in case parts of the shorthand affect // the computed values of other parts. for (idx in info.subproperties) { var subprop = info.subproperties[idx]; is(gComputedStyle.getPropertyValue(subprop), step1comps[idx], "serialize(" + subprop + ")+parse should be the identity " + "transform for '" + property + ": " + value + "'"); } is(gDeclaration.getPropertyValue(property), step1val, "parse+split+serialize should be idempotent for '" + property + ": " + value + "'"); } if (info.type != CSS_TYPE_TRUE_SHORTHAND && property != "mask") { gDeclaration.removeProperty(property); gDeclaration.setProperty(property, step1comp, ""); var func = xfail_compute(property, value) ? todo_is : is; func(gComputedStyle.getPropertyValue(property), step1comp, "parse+compute+serialize should be idempotent for '" + property + ": " + value + "'"); } if ("subproperties" in info) { gDeclaration.removeProperty(property); for (idx in info.subproperties) { var subprop = info.subproperties[idx]; gDeclaration.setProperty(subprop, step1comps[idx], ""); } // Now that all the subprops are set, check their values. Note that we // need this in a separate loop, in case parts of the shorthand affect // the computed values of other parts. for (idx in info.subproperties) { var subprop = info.subproperties[idx]; is(gComputedStyle.getPropertyValue(subprop), step1comps[idx], "parse+compute+serialize(" + subprop + ") should be idempotent for '" + property + ": " + value + "'"); } } // sanity check shorthands to make sure disabled props aren't exposed if (info.type != CSS_TYPE_LONGHAND) { gDeclaration.setProperty(property, value, ""); test_remove_all_properties(property, value); } gDeclaration.removeProperty(property); } function test_value_without_variable(value) { test_value(value, null); } function test_value_with_variable(value) { gPrereqDeclaration.setProperty("--a", value, ""); test_value("var(--a)", value); gPrereqDeclaration.removeProperty("--a"); } if ("prerequisites" in info) { var prereqs = info.prerequisites; for (var prereq in prereqs) { gPrereqDeclaration.setProperty(prereq, prereqs[prereq], ""); } } var idx; for (idx in info.initial_values) { test_value_without_variable(info.initial_values[idx]); test_value_with_variable(info.initial_values[idx]); } for (idx in info.other_values) { test_value_without_variable(info.other_values[idx]); test_value_with_variable(info.other_values[idx]); } if ("prerequisites" in info) { for (var prereq in info.prerequisites) { gPrereqDeclaration.removeProperty(prereq); } } } function runTest() { // To avoid triggering the slow script dialog, we have to test one // property at a time. ok(SpecialPowers.getBoolPref("layout.css.variables.enabled"), "pref not set #1"); var props = []; for (var prop in gCSSProperties) { var info = gCSSProperties[prop]; if ("subproperties" in info) { for (var idx in info.subproperties) { var subprop = info.subproperties[idx]; if (!(subprop in gPropertyShorthands)) { gPropertyShorthands[subprop] = []; } gPropertyShorthands[subprop].push(prop); } } props.push(prop); } props = props.reverse(); function do_one() { if (props.length == 0) { SimpleTest.finish(); return; } test_property(props.pop()); SimpleTest.executeSoon(do_one); } SimpleTest.executeSoon(do_one); } SimpleTest.waitForExplicitFinish(); SimpleTest.requestLongerTimeout(7); SpecialPowers.pushPrefEnv({ set: [["layout.css.variables.enabled", true]] }, runTest); </script> </pre> </body> </html>