<!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>