Bug 1273706 - Part 30: Add preliminary tests for the Properties & Values API implementation. r?heycam draft
authorJonathan Chan <jyc@eqv.io>
Thu, 18 Aug 2016 15:31:34 -0700
changeset 402919 ea6c2b639d29faab3fd477e717a59d6204f775ca
parent 402918 ee6d7779fe92e3271bc2df5bafb2b1ba3ee75b80
child 528780 80617fb333dc18843a8862254d7efe3a371151ca
push id26775
push userjchan@mozilla.com
push dateThu, 18 Aug 2016 22:38:41 +0000
reviewersheycam
bugs1273706
milestone51.0a1
Bug 1273706 - Part 30: Add preliminary tests for the Properties & Values API implementation. r?heycam MozReview-Commit-ID: 83EJlZqvNhZ
layout/reftests/css-properties-and-values/properties-and-values-01-ref.html
layout/reftests/css-properties-and-values/properties-and-values-01a.html
layout/reftests/css-properties-and-values/properties-and-values-01b.html
layout/reftests/css-properties-and-values/properties-and-values-02-ref.html
layout/reftests/css-properties-and-values/properties-and-values-02.html
layout/reftests/css-properties-and-values/properties-and-values-03-ref.html
layout/reftests/css-properties-and-values/properties-and-values-03.html
layout/reftests/css-properties-and-values/reftest.list
layout/reftests/reftest.list
testing/web-platform/meta/MANIFEST.json
testing/web-platform/meta/css-houdini/properties-and-values.html.ini
testing/web-platform/tests/css-houdini/properties-and-values.html
new file mode 100644
--- /dev/null
+++ b/layout/reftests/css-properties-and-values/properties-and-values-01-ref.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title>CSS Properties &amp; Values API - Reference #1</title>
+  <style>
+div#mydiv {
+  background-color: green;
+}
+span#myspan {
+  color: white;
+  background-color: blue;
+}
+a {
+  color: white;
+  background-color: red;
+}
+  </style>
+</head>
+<body>
+  <div id="mydiv">
+    I should be green, not purple or orange.
+    <span id="myspan">
+      I should be blue, not green (means I used something in the cascade) or red (means I used the initial value)!
+      <a>
+        I should be red.
+      </a>
+    </span>
+  </div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/css-properties-and-values/properties-and-values-01a.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title>CSS Properties &amp; Values API - Test #1a</title>
+  <script>
+CSS.registerProperty({name: "--a", syntax: "<color>", inherits: true, initialValue: "green"});
+CSS.registerProperty({name: "--b", syntax: "<color>", inherits: false, initialValue: "red"});
+  </script>
+  <style>
+div {
+  --a: purple;
+  background-color: var(--a);
+  --b: blue;
+}
+div#mydiv {
+  --a: orange;
+  --a: initial;
+}
+span {
+  color: white;
+  background-color: var(--b);
+  --b: green;
+}
+span#myspan {
+  --b: inherit;
+}
+a {
+  background-color: var(--b);
+}
+  </style>
+</head>
+<body>
+  <div id="mydiv">
+    <!--
+      This should be green because we registered ––a with initial value |green|.
+      It would be purple if we used the |––a: purple| declaration from higher
+      up in the cascade in the same place we declared background-color, or
+      orange if we used the earlier declaration|'––a: orange| in the same
+      declaration block.
+    -->
+    I should be green, not purple or orange.
+    <span id="myspan">
+      <!--
+        This should be blue because of the declaration |––b: inherit|, which
+        should cause us to inherit the declaration |––b: blue| from the |div|
+        declaration block.
+      -->
+      I should be blue, not green (means I used something in the cascade) or red (means I used the initial value)!
+      <a>
+        <!--
+          This should be red because |––b| is not inherited, and its initial value is red.
+          It would be blue if we inherited our parent element #myspan's value.
+        -->
+        I should be red.
+      </a>
+    </span>
+  </div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/css-properties-and-values/properties-and-values-01b.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<!-- The same as properties-and-values-01a, but with the property registrations
+     at the end of the page, so stylesheets will already have been loaded. -->
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title>CSS Properties &amp; Values API - Test #1b</title>
+  <style>
+div {
+  --a: purple;
+  background-color: var(--a);
+  --b: blue;
+}
+div#mydiv {
+  --a: orange;
+  --a: initial;
+}
+span {
+  color: white;
+  background-color: var(--b);
+  --b: green;
+}
+span#myspan {
+  --b: inherit;
+}
+a {
+  background-color: var(--b);
+}
+  </style>
+</head>
+<body>
+  <div id="mydiv">
+    <!--
+      This should be green because we registered ––a with initial value |green|.
+      It would be purple if we used the |––a: purple| declaration from higher
+      up in the cascade in the same place we declared background-color, or
+      orange if we used the earlier declaration|'––a: orange| in the same
+      declaration block.
+    -->
+    I should be green, not purple or orange.
+    <span id="myspan">
+      <!--
+        This should be blue because of the declaration |––b: inherit|, which
+        should cause us to inherit the declaration |––b: blue| from the |div|
+        declaration block.
+      -->
+      I should be blue, not green (means I used something in the cascade) or red (means I used the initial value)!
+      <a>
+        <!--
+          This should be red because |––b| is not inherited, and its initial value is red.
+          It would be blue if we inherited our parent element #myspan's value.
+        -->
+        I should be red.
+      </a>
+    </span>
+  </div>
+  <script>
+CSS.registerProperty({name: "--a", syntax: "<color>", inherits: true, initialValue: "green"});
+CSS.registerProperty({name: "--b", syntax: "<color>", inherits: false, initialValue: "red"});
+  </script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/css-properties-and-values/properties-and-values-02-ref.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title>CSS Properties &amp; Values API - Reference #2</title>
+<style>
+span {
+  color: green;
+}
+</style>
+</head>
+<body>
+  <div>
+    <span>
+      This text should be green.
+    </span>
+  </div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/css-properties-and-values/properties-and-values-02.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title>CSS Properties &amp; Values API - Test #2</title>
+  <script>
+window.onload = function () {
+  CSS.registerProperty({name: "--a", syntax: "<color>", inherits: false, initialValue: "green"});
+}
+  </script>
+<style>
+div {
+  --a: red;
+}
+span {
+  color: var(--a);
+}
+</style>
+</head>
+<body>
+  <div>
+    <span>
+      This text should be green.
+    </span>
+  </div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/css-properties-and-values/properties-and-values-03-ref.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title>CSS Properties &amp; Values API - Test #3</title>
+  <style>
+span {
+  color: white;
+  background-color: green;
+}
+  </style>
+</head>
+<body>
+  <div>
+    <span id="myspan">
+      This box should be green.
+    </span>
+  </div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/css-properties-and-values/properties-and-values-03.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<!--
+  How is this test different from properties-and-values-02.html? While writing
+  the patches, I ran into a bug where this behavior would fail with reset (e.g.
+  background-color's) style structs but not with inherited (e.g. color's) style
+  structs.
+-->
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <title>CSS Properties &amp; Values API - Test #3</title>
+  <script>
+window.onload = function () {
+  CSS.registerProperty({name: "--b", syntax: "<color>", inherits: false, initialValue: "green"});
+}
+  </script>
+  <style>
+div {
+  --b: red;
+}
+span {
+  color: white;
+  background-color: var(--b);
+}
+  </style>
+</head>
+<body>
+  <div>
+    <span id="myspan">
+      This box should be green.
+    </span>
+  </div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/css-properties-and-values/reftest.list
@@ -0,0 +1,6 @@
+default-preferences pref(layout.css.properties_and_values.enabled,true)
+
+== properties-and-values-01a.html properties-and-values-01-ref.html
+== properties-and-values-01b.html properties-and-values-01-ref.html
+== properties-and-values-02.html properties-and-values-02-ref.html
+== properties-and-values-03.html properties-and-values-03-ref.html
--- a/layout/reftests/reftest.list
+++ b/layout/reftests/reftest.list
@@ -134,16 +134,19 @@ include css-ui-invalid/reftest.list
 include css-ui-valid/reftest.list
 
 # css values and units
 include css-valuesandunits/reftest.list
 
 # css variables
 include css-variables/reftest.list
 
+# css properties & values
+include css-properties-and-values/reftest.list
+
 # Reftests in css-visited are run using
 # layout/style/test/test_visited_reftests instead of using the reftest
 # harness.
 
 include cssom/reftest.list
 
 # columns/
 include columns/reftest.list
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -12637,24 +12637,24 @@
         "path": "XMLHttpRequest/data-uri.htm",
         "url": "/XMLHttpRequest/data-uri.htm"
       },
       {
         "path": "XMLHttpRequest/event-abort.htm",
         "url": "/XMLHttpRequest/event-abort.htm"
       },
       {
+        "path": "XMLHttpRequest/event-error-order.sub.html",
+        "url": "/XMLHttpRequest/event-error-order.sub.html"
+      },
+      {
         "path": "XMLHttpRequest/event-error.sub.html",
         "url": "/XMLHttpRequest/event-error.sub.html"
       },
       {
-        "path": "XMLHttpRequest/event-error-order.sub.html",
-        "url": "/XMLHttpRequest/event-error-order.sub.html"
-      },
-      {
         "path": "XMLHttpRequest/event-load.htm",
         "url": "/XMLHttpRequest/event-load.htm"
       },
       {
         "path": "XMLHttpRequest/event-loadend.htm",
         "url": "/XMLHttpRequest/event-loadend.htm"
       },
       {
@@ -12669,24 +12669,24 @@
         "path": "XMLHttpRequest/event-readystate-sync-open.htm",
         "url": "/XMLHttpRequest/event-readystate-sync-open.htm"
       },
       {
         "path": "XMLHttpRequest/event-readystatechange-loaded.htm",
         "url": "/XMLHttpRequest/event-readystatechange-loaded.htm"
       },
       {
+        "path": "XMLHttpRequest/event-timeout-order.htm",
+        "url": "/XMLHttpRequest/event-timeout-order.htm"
+      },
+      {
         "path": "XMLHttpRequest/event-timeout.htm",
         "url": "/XMLHttpRequest/event-timeout.htm"
       },
       {
-        "path": "XMLHttpRequest/event-timeout-order.htm",
-        "url": "/XMLHttpRequest/event-timeout-order.htm"
-      },
-      {
         "path": "XMLHttpRequest/event-upload-progress-crossorigin.sub.htm",
         "url": "/XMLHttpRequest/event-upload-progress-crossorigin.sub.htm"
       },
       {
         "path": "XMLHttpRequest/event-upload-progress.htm",
         "url": "/XMLHttpRequest/event-upload-progress.htm"
       },
       {
@@ -37619,16 +37619,28 @@
                 "=="
               ]
             ],
             "url": "/html/semantics/grouping-content/the-ol-element/reversed-1d.html"
           }
         ]
       },
       "testharness": {
+        "XMLHttpRequest/event-error.html": [
+          {
+            "path": "XMLHttpRequest/event-error.html",
+            "url": "/XMLHttpRequest/event-error.html"
+          }
+        ],
+        "css-houdini/properties-and-values.html": [
+          {
+            "path": "css-houdini/properties-and-values.html",
+            "url": "/css-houdini/properties-and-values.html"
+          }
+        ],
         "css-shapes/basic-shape-circle-ellipse-serialization.html": [
           {
             "path": "css-shapes/basic-shape-circle-ellipse-serialization.html",
             "url": "/css-shapes/basic-shape-circle-ellipse-serialization.html"
           }
         ],
         "html/semantics/forms/the-form-element/form-submission-sandbox.html": [
           {
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/meta/css-houdini/properties-and-values.html.ini
@@ -0,0 +1,3 @@
+[properties-and-values.html]
+  type: testharness
+  prefs: [layout.css.properties_and_values.enabled:true]
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/css-houdini/properties-and-values.html
@@ -0,0 +1,988 @@
+<!doctype html>
+<meta charset=utf-8>
+<title></title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+
+<style type="text/css">
+body {
+  font-size: 16px;
+}
+
+@keyframes number {
+  from { --number: 0; }
+  to { --number: 1; }
+}
+
+@keyframes transform {
+  from { --transform: rotate(0deg); }
+  to { --transform: rotate(180deg); }
+}
+
+@keyframes list1 {
+  from { --list: 0 0; }
+  to { --list: 1 2; }
+}
+
+@keyframes lp {
+  from { --lp: calc(0 + 15%); }
+  to { --lp: calc(45% + 14); }
+}
+
+@keyframes length {
+  from { --length: 0px; }
+  to { --length: 10px; }
+}
+
+@keyframes percentage {
+  from { --percentage: 0%; }
+  to { --percentage: 100%; }
+}
+
+@keyframes integer {
+  from { --integer: 0; }
+  to { --integer: 10; }
+}
+
+@keyframes lps1 {
+  from { --lps: 0px 0% calc(0px + 0%); }
+  to { --lps: 100px 100% calc(100px + 100%); }
+}
+
+@keyframes lps2 {
+  from { --lps: calc(50px + 0%) calc(0px + 50%) calc(50px + 50%); }
+  to { --lps: calc(100px + 0%) calc(0px + 100%) calc(0px + 0%); }
+}
+
+@keyframes missing {
+  from { --number: 0; }
+}
+</style>
+
+<div id="grandparent">
+  <div id="parent">
+    <div id="child"></div>
+  </div>
+</div>
+<div id="sandbox"></div>
+
+<a id="link" href="/"></a>
+
+<script>
+let baseURL = document.getElementById("link").href;
+
+let animTests = [
+  { keyframes: "number",
+    property: "--number",
+    syntax: "<number>",
+    halfway: "0.5"
+  },
+  { keyframes: "transform",
+    property: "--transform",
+    syntax: "<transform-function>",
+    halfway: "rotate(90deg)"
+  },
+  { keyframes: "list1",
+    property: "--list",
+    syntax: "<number>+",
+    halfway: "0.5 1.0"
+  },
+  { keyframes: "lp",
+    property: "--lp",
+    syntax: "<length-percentage>",
+    halfway: "calc(7 + 30%)"
+  },
+  { keyframes: "length",
+    property: "--length",
+    syntax: "<length>",
+    halfway: "5px",
+  },
+  { keyframes: "percentage",
+    property: "--percentage",
+    syntax: "<percentage>",
+    halfway: "50%",
+  },
+  { keyframes: "integer",
+    property: "--integer",
+    syntax: "<integer>",
+    halfway: "5",
+  },
+  { keyframes: "lps1",
+    property: "--lps",
+    syntax: "<length-percentage>+",
+    halfway: "50px 50% calc(50px + 50%)",
+  },
+  { keyframes: "lps2",
+    property: "--lps",
+    syntax: "<length-percentage>+",
+    halfway: "calc(75px + 0%) calc(0px + 75%) calc(25px + 25%)",
+  },
+];
+
+let animTests2 = [
+  { syntax: "<number>",
+    keyframes: [
+      "0",
+      "1",
+    ],
+    checks: {
+      "0.4": "0.4",
+      "0.6": "0.6",
+    },
+  },
+
+  // These are uninterpolable values.
+  // They should be flip at 50%.
+
+  { syntax: "<angle>",
+    keyframes: [
+      "0deg",
+      "90deg",
+    ],
+    checks: {
+      "0.4": "0deg",
+      "0.6": "90deg",
+    },
+  },
+
+  { syntax: "<time>",
+    keyframes: [
+      "3s",
+      "9s",
+    ],
+    checks: {
+      "0.4": "3s",
+      "0.6": "9s",
+    },
+  },
+
+  { syntax: "<resolution>",
+    keyframes: [
+      "96dpi",
+      "144dpi",
+    ],
+    checks: {
+      "0.4": "96dpi",
+      "0.6": "144dpi",
+    },
+  },
+
+  { syntax: "<custom-ident>",
+    keyframes: [
+      "doing-good",
+      "is-in-our-code",
+    ],
+    checks: {
+      "0.4": "doing-good",
+      "0.6": "is-in-our-code",
+    },
+  },
+
+  { syntax: "Mozilla | Firefox",
+    keyframes: [
+      "Mozilla",
+      "Firefox",
+    ],
+    checks: {
+      "0.4": "Mozilla",
+      "0.6": "Firefox",
+    },
+  },
+
+  // <url>s need to carry around a principal too, so it's worth testing that we
+  // are able to pass them around correctly (the others are all just unparsed
+  // strings).
+  { syntax: "<url>",
+    keyframes: [
+      "url(\"http://mozilla.com/\")",
+      "url(\"http://firefox.com/\")",
+    ],
+    checks: {
+      "0.4": "url(\"http://mozilla.com/\")",
+      "0.6": "url(\"http://firefox.com/\")",
+    },
+  },
+
+  { syntax: "*",
+    keyframes: [
+      "youve never seen the real numbers",
+      "like this before",
+    ],
+    checks: {
+      "0.4": "youve never seen the real numbers",
+      "0.6": "like this before",
+    },
+  },
+];
+
+// { registrations: [
+//     { name: <custom property name with '--' prefix>,
+//       syntax: <custom property syntax>,
+//       inherits: bool,
+//       initialValue: <computationally idempotent initial value>
+//     },
+//     ...
+//   ],
+//   grandparent: <style for grandparent element>,
+//   parent: <style for parent element>,
+//   child: <style for child element>,
+//   expected: {
+//     <computed property>: <expected value string>,
+//     ...
+//   },
+// }
+let familyTests = [
+  // Basic uninherited property.
+  { registrations: [
+      { name: "--a",
+        syntax: "<color>",
+        inherits: false,
+        initialValue: "red",
+      },
+    ],
+    grandparent: "--a: green; --a: red;",
+    parent: "--a: inherit;",
+    expected: {
+      "--a": "red",
+    },
+  },
+  // Transform function arguments should be computed.
+  { registrations: [
+      { name: "--a",
+        syntax: "<transform-function>",
+        inherits: true,
+      },
+    ],
+    parent: "font-size: 10px;",
+    child: "--a: translateX(5em);",
+    expected: {
+      "--a": "translateX(50px)",
+    },
+  },
+  // If no initialValue is provided and the syntax is *, then a special
+  // initial value used. This initial value must be considered parseable by
+  // registerProperty() but invalid at computed value time.
+  { registrations: [
+      { name: "--a",
+        syntax: "*",
+        inherits: true,
+      },
+    ],
+    parent: "--b: happy birthday; --b: hi var(--a);",
+    expected: {
+      // 'happy birthday' is overwritten in the cascade, and the var(--a)
+      // substitution is illegal.
+      "--b": "",
+    },
+  },
+  // Same as the previous test, with a fallback (i.e. we shouldn't quit before
+  // resolving).
+  { registrations: [
+      { name: "--a",
+        syntax: "*",
+        inherits: true,
+      },
+    ],
+    parent: "--b: happy birthday; --b: hi var(--a,to you);",
+    expected: {
+      // The leading space is part of the value!
+      "--b": " hi to you",
+    },
+  },
+  // Make sure we calculate font-relative lengths in lengths and
+  // length-percentages.
+  { registrations: [
+      { name: "--a",
+        syntax: "<length>",
+      },
+      { name: "--b",
+        syntax: "<length-percentage>",
+      },
+    ],
+    parent: "font-size: 15px",
+    child: "--a: 1em; --b: calc(1em + 20%);",
+    expected: {
+      "--a": "15px",
+      "--b": "calc(15px + 20%)",
+    },
+  },
+  // Make sure we set initial values on the root, even if nothing is ever
+  // specified.
+  { registrations: [
+      { name: "--a",
+        syntax: "<number>",
+        initialValue: "42",
+      },
+    ],
+    expected: {
+      "--a": "42",
+    },
+  },
+];
+
+
+let idempotentPassTests = [
+  // https://github.com/w3c/css-houdini-drafts/issues/247
+  { syntax: "<color>", initialValue: "currentColor" },
+  { syntax: "<length>", initialValue: "5px" },
+  { syntax: "<length>", initialValue: "calc(5px + 7px)" },
+  { syntax: "<transform-function>", initialValue: "translateX(5px)" },
+  { syntax: "<transform-function>+", initialValue: "translateX(5px) translateX(-10px)" },
+];
+
+let idempotentFailTests = [
+  { syntax: "<color>", initialValue: "inherit" },
+  { syntax: "<length>", initialValue: "5em" },
+  { syntax: "<length>", initialValue: "calc(5px + 5em)" },
+  { syntax: "<transform-function>", initialValue: "translateX(5em)" },
+  { syntax: "<integer> | <length>+", initialValue: "5px 0 -5em 0 12px" },
+  { syntax: "<color> | <transform-function>+", initialValue: "translateX(-10em)" },
+];
+
+let invalidSyntaxTests = [
+  { syntax: "<number>+ <color>" },
+  { syntax: "<i-don't-exist>+" },
+  { syntax: "" },
+  { syntax: "<number> || <color>" },
+  { syntax: "|"},
+  { syntax: "+", initialValue: "calc(5 + 7)" },
+  { syntax: "   ", initialValue: "calc(5 + 7)" },
+  { syntax: "initial" },
+  { syntax: "inherit" },
+  { syntax: "unset" },
+  // We don't support revert yet.
+  //{ syntax: "revert" },
+  { syntax: "* worn out places" },
+  { syntax: "* | worn | out | faces" },
+  { syntax: "*+" },
+];
+
+let validSyntaxTests = [
+  { syntax: "*" },
+  { syntax: "<color>" },
+  { syntax: " <color> " },
+  { syntax: "<color>  +" },
+
+  // Allow some CSS keywords, just not the CSS-wide keywords or revert.
+  { syntax: "center" },
+];
+
+let syntaxFailTests = [
+  { syntax: "<color>", initialValue: "5px" },
+  { syntax: "<transform-function>", initialValue: "red" },
+  { syntax: "<transform-function>+", initialValue: " " },
+  { syntax: "<resolution>", initialValue: "calc(0)" },
+  { syntax: "<custom-ident>", initialValue: "5px" },
+  { syntax: "to-me", initialValue: "who-are-you" },
+  // https://drafts.csswg.org/css-variables/#syntax
+  // "While <declaration-value> must represent at least one token, that one
+  //  token may be whitespace. This implies that --foo: ; is valid, and the
+  //  corresponding var(--foo) call would have a single space as its
+  //  substitution value, but --foo:; is invalid."
+  { syntax: "*", initialValue: "" },
+];
+
+let syntaxPassTests = [
+  { syntax: "*", initialValue: "all around me are familiar faces" },
+  { syntax: "*", initialValue: "translateX(5px) 5em 73" },
+  { syntax: "<number>", initialValue: "5" },
+  { syntax: "<number> | <color>", initialValue: "red" },
+  { syntax: "<number>+", initialValue: "4 8 15 16 23 42" },
+  { syntax: "<number>+ | <url>", initialValue: "url(http://mozilla.org)" },
+  { syntax: "<transform-function>", initialValue: "translateX(5px)" },
+  { syntax: "<resolution>", initialValue: "5dpi" },
+  { syntax: "<custom-ident>", initialValue: "doesnt-really-matter" },
+  { syntax: "to-me", initialValue: "to-me" },
+  { syntax: "to-me | just-a-poor-boy", initialValue: "just-a-poor-boy" },
+  // https://drafts.csswg.org/css-variables/#syntax
+  // "While <declaration-value> must represent at least one token, that one
+  //  token may be whitespace. This implies that --foo: ; is valid, and the
+  //  corresponding var(--foo) call would have a single space as its
+  //  substitution value, but --foo:; is invalid."
+  { syntax: "*", initialValue: " " },
+];
+
+let invalidNameTests = [
+  // https://drafts.csswg.org/css-syntax-3/#typedef-ident-token
+  { name: "--" },
+  { name: "-" },
+  { name: " " },
+  { name: "--5" },
+  { name: "--!" },
+];
+
+let validNameTests = [
+  { name: "--a" },
+  { name: "--A" },
+  { name: "--ç" },
+  { name: "--\\n" },
+];
+
+// https://drafts.csswg.org/cssom/#serialize-a-css-value
+
+let computedValueTests = [
+  // Parsing & computing (incl. serializing) lists of idents.
+  {
+    syntax: "a+ | <custom-ident>+",
+    declared: "a a a a b c d",
+    expected: "a a a a b c d",
+  },
+  // Angle units by themselves and in transform functions.
+  {
+    syntax: "<angle>",
+    declared: "0deg",
+    expected: "0deg",
+  },
+  {
+    syntax: "<angle>",
+    declared: "0grad",
+    expected: "0grad",
+  },
+  {
+    syntax: "<angle>",
+    declared: "0rad",
+    expected: "0rad",
+  },
+  {
+    syntax: "<angle>",
+    declared: "0turn",
+    expected: "0turn",
+  },
+  {
+    syntax: "<transform-function>",
+    declared: "rotate(0deg)",
+    expected: "rotate(0deg)",
+  },
+  {
+    syntax: "<transform-function>",
+    declared: "rotate(0grad)",
+    expected: "rotate(0grad)",
+  },
+  {
+    syntax: "<transform-function>",
+    declared: "rotate(0rad)",
+    expected: "rotate(0rad)",
+  },
+  {
+    syntax: "<transform-function>",
+    declared: "rotate(0turn)",
+    expected: "rotate(0turn)",
+  },
+  // Correctly serializing unitless 0 in '<length> | <percentage>'.
+  { syntax: "<length> | <percentage>",
+    declared: "0",
+    expected: "0px",
+  },
+
+  // Normalization of <length-percentage> values [1]
+  // [1]: https://drafts.css-houdini.org/css-properties-values-api/#calculation-of-computed-values
+
+  { syntax: "<length-percentage>",
+    declared: "calc(70% - 70% + 20px)",
+    expected: "calc(20px + 0%)",
+  },
+  {
+    syntax: "<length-percentage>",
+    declared: "calc(20px - 20px - 30%)",
+    expected: "calc(0px + -30%)",
+  },
+  // Should simplify calc(Xpx) to Xpx
+  { syntax: "<length-percentage>",
+    declared: "calc(20px - 17px)",
+    expected: "3px",
+  },
+  // Should simplify calc(X%) to X%
+  { syntax: "<length-percentage>",
+    declared: "calc(15% + 5%)",
+    expected: "20%",
+  },
+  { syntax: "<percentage> | <length>",
+    declared: "calc(15px)",
+    expected: "15px",
+  },
+  { syntax: "<length> | <percentage>",
+    declared: "calc(15%)",
+    expected: "15%",
+  },
+
+  // Test basic computation of each of the basic syntax strings.
+
+  { syntax: "<length>",
+    declared: "10px",
+    expected: "10px",
+  },
+
+  { syntax: "<number>",
+    declared: "3.1415",
+    expected: "3.1415",
+  },
+  { syntax: "<number>",
+    declared: "calc(1 * 2 + 3 / 4)",
+    expected: "2.75",
+  },
+  { syntax: "<number>",
+    declared: "calc(1 / 0)",
+    expected: "",
+  },
+
+  { syntax: "<percentage>",
+    declared: "2.7%",
+    expected: "2.7%",
+  },
+
+  { syntax: "<length-percentage>",
+    declared: "calc(13px + 27%)",
+    expected: "calc(13px + 27%)",
+  },
+
+  { syntax: "<color>",
+    declared: "#abcdef01",
+    expected: "rgba(171, 205, 239, 0.004)",
+  },
+  { syntax: "<color>",
+    declared: "red",
+    expected: "red",
+  },
+  { syntax: "<color>",
+    declared: "currentcolor",
+    expected: "currentcolor",
+  },
+
+  { syntax: "<image>",
+    declared: "url(\"http://example.com/some_image.jpg\")",
+    expected: "url(\"http://example.com/some_image.jpg\")",
+  },
+
+  { syntax: "<image>",
+    declared: "linear-gradient(45deg, white, black)",
+    expected: "linear-gradient(45deg, rgb(255, 255, 255), rgb(0, 0, 0))",
+  },
+
+  { syntax: "<image>",
+    declared: "radial-gradient(2em at 60px 50% , #000000 0%, #000000 14px, rgba(0, 0, 0, 0.3) 18px, rgba(0, 0, 0, 0) 4em)",
+    expected: "radial-gradient(32px at 60px 50%, rgb(0, 0, 0) 0%, rgb(0, 0, 0) 14px, rgba(0, 0, 0, 0.3) 18px, transparent 64px)",
+    // This is the serialization result we get from calling getComputedStyle
+    // and getPropertyValue on a background-image property set to this value.
+  },
+
+  { syntax: "<url>",
+    declared: "url(\"http://example.com/\")",
+    expected: "url(\"http://example.com/\")",
+  },
+
+  { syntax: "<url>",
+    declared: "url(\"/some_link\")",
+    expected: "url(\"" + baseURL + "some_link\")",
+  },
+
+  { syntax: "<integer>",
+    declared: "42",
+    expected: "42",
+  },
+  { syntax: "<integer>",
+    declared: "calc(1 + 2 * 3 / 6)",
+    // Any divisions in <integer> calc()s cause the whole thing to be invalid at parse time.
+    expected: "",
+  },
+  { syntax: "<integer>",
+    declared: "calc(1 + 2 * 3)",
+    expected: "7",
+  },
+  { syntax: "<integer>",
+    declared: "calc(1 + 2 + 3)",
+    expected: "6",
+  },
+  { syntax: "<integer>",
+    declared: "calc(1 * 2 * 3)",
+    expected: "6",
+  },
+  { syntax: "<integer>",
+    declared: "calc(0.5 * 2 * 3)",
+    expected: "",
+  },
+  // Additive coefficients should be resolved.
+  { syntax: "<integer>",
+    declared: "calc((1 + 2) * 3)",
+    expected: "9",
+  },
+  { syntax: "<integer>",
+    declared: "calc((1 * 2) * 3)",
+    expected: "6",
+  },
+  { syntax: "<integer>",
+    declared: "calc(1 * (2 * 3))",
+    expected: "6",
+  },
+  // This isn't unitless zero -- this is an integral zero!
+  { syntax: "<integer>",
+    declared: "calc(0)",
+    expected: "0",
+  },
+  { syntax: "<integer>",
+    declared: "calc(((0) * 3) + 7)",
+    expected: "7",
+  },
+
+  { syntax: "<angle>",
+    declared: "37deg",
+    expected: "37deg",
+  },
+
+  { syntax: "<time>",
+    declared: "5s",
+    expected: "5s",
+  },
+
+  { syntax: "<resolution>",
+    declared: "50dppx",
+    expected: "50dppx",
+  },
+
+  { syntax: "<transform-function>",
+    declared: "matrix(1, 0, 1, 0, 1, 1)",
+    expected: "matrix(1, 0, 1, 0, 1, 1)",
+  },
+
+  { syntax: "<transform-function>",
+    declared: "matrix(1, 0, 1, 0, 1, 1)",
+    expected: "matrix(1, 0, 1, 0, 1, 1)",
+  },
+
+  { syntax: "Mozilla | Firefox",
+    declared: "Mozilla",
+    expected: "Mozilla",
+  },
+
+  { syntax: "<custom-ident>",
+    declared: "Mozilla",
+    expected: "Mozilla",
+  },
+
+  { syntax: "*",
+    declared: " ",
+    expected: " ",
+  },
+
+  { syntax: "*",
+    declared: "i was the shadow of the waxwing slain",
+    expected: "i was the shadow of the waxwing slain",
+  },
+
+  // https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-getpropertyvalue
+  // We should return the empty string when there is no declaration (in this
+  // case, beacuse couldn't compute.)
+
+  { syntax: "<length>",
+    declared: "calc(5px + 7%)",
+    expected: "",
+  },
+
+  { syntax: "<percentage>",
+    declared: "calc(7% + 5px)",
+    expected: "",
+  },
+
+  { syntax: "<length>",
+    declared: "Mozilla",
+    expected: "",
+  },
+
+  { syntax: "<length>+",
+    declared: "5px calc(5px + 7%) 1em",
+    // This should be invalid at parse time.
+    expected: "",
+  },
+];
+
+// Testing functions.
+
+// Animation tests.
+
+let grandparent = document.getElementById("grandparent");
+let parent = document.getElementById("parent");
+let child = document.getElementById("child");
+let sandbox = document.getElementById("sandbox");
+
+for (let spec of animTests) {
+  let { keyframes, property, syntax, halfway } = spec;
+  test(function () {
+    try {
+      CSS.registerProperty({ name: property, syntax: syntax });
+
+      // Store into |computedExpected| the computed value we get from specifying
+      // |halfway|. This should normalize the value.
+      sandbox.style.setProperty(property, halfway);
+      let computedExpected = window.getComputedStyle(sandbox).getPropertyValue(property);
+      sandbox.style.removeProperty(property);
+
+      grandparent.style.setProperty("animation-name", keyframes);
+      grandparent.style.setProperty("animation-duration", "1s");
+      grandparent.style.setProperty("animation-timing-function", "linear");
+
+      let anim = grandparent.getAnimations()[0];
+      anim.pause();
+      anim.currentTime = 500;
+
+      let computed = window.getComputedStyle(grandparent).getPropertyValue(property);
+      assert_equals(computed, computedExpected, "halfway value equals expected value");
+    } finally {
+      grandparent.style.removeProperty("animation-name");
+      grandparent.style.removeProperty("animation-duration");
+      grandparent.style.removeProperty("animation-timing-function");
+      CSS.unregisterProperty(property);
+    }
+  }, "anim: " + JSON.stringify(spec));
+}
+
+for (let spec of animTests2) {
+  let { keyframes, initial, syntax, checks } = spec;
+  test(function () {
+    try {
+      CSS.registerProperty({ name: "--test", syntax: syntax });
+
+      let anim = grandparent.animate(keyframes.map(value => ({ "--test": value })), 1000);
+      try {
+        anim.pause();
+        for (let point in checks) {
+          let time = parseFloat(point);
+          anim.currentTime = time * 1000;
+          let computed = window.getComputedStyle(grandparent).getPropertyValue("--test");
+          assert_equals(computed, checks[point], `value at time ${time} equals expected value`);
+        }
+      } finally {
+        anim.cancel();
+      }
+    } finally {
+      grandparent.style.removeProperty("--test");
+      CSS.unregisterProperty("--test");
+    }
+  }, "anim #2: " + JSON.stringify(spec));
+}
+
+// Make sure that we fill in missing keyframe values.
+// It's possible to have custom properties without initial values (i.e. they
+// have invalid initial values).
+// CSS Animations says:
+//   If a ‘0%’ or ‘from’ keyframe is not specified, then the user agent
+//   constructs a ‘0%’ keyframe using the computed values of the properties
+//   being animated. If a ‘100%’ or ‘to’ keyframe is not specified, then the
+//   user agent constructs a ‘100%’ keyframe using the computed values of the
+//   properties being animated.
+// But for a custom property without an initial value, it's not possible to
+// fill in a value.
+
+test(function () {
+  CSS.registerProperty({ name: "--number", syntax: "<number>" });
+
+  grandparent.style.setProperty("animation-name", "missing");
+  grandparent.style.setProperty("animation-duration", "1s");
+  grandparent.style.setProperty("animation-timing-function", "linear");
+
+  let anim = grandparent.getAnimations()[0];
+  anim.pause();
+  anim.currentTime = 500;
+
+  try {
+    assert_equals(window.getComputedStyle(grandparent)
+                        .getPropertyValue("--number"),
+                  "",
+                  "There should be no animation if there is no property to " +
+                  "fall back on.");
+    // Shouldn't segfault while uncomputing values either!
+    for (let keyframe of anim.effect.getKeyframes()) {
+      assert_equals(keyframe["--number"], undefined,
+        "Shouldn't have --number in keyframes when we were missing a " +
+        "keyframe value.");
+    }
+  } finally {
+    CSS.unregisterProperty("--number");
+    grandparent.style.removeProperty("animation-name");
+    grandparent.style.removeProperty("animation-duration");
+    grandparent.style.removeProperty("animation-timing-function");
+  }
+}, "Custom properties without initial values missing in keyframes should be " +
+   "removed.");
+
+test(function () {
+  CSS.registerProperty({ name: "--number", syntax: "<number>" });
+
+  grandparent.style.setProperty("animation-name", "missing");
+  grandparent.style.setProperty("animation-duration", "1s");
+  grandparent.style.setProperty("animation-timing-function", "linear");
+  grandparent.style.setProperty("--number", "1");
+
+  let anim = grandparent.getAnimations()[0];
+  anim.pause();
+  anim.currentTime = 500;
+
+  try {
+    assert_equals(window.getComputedStyle(grandparent)
+                        .getPropertyValue("--number"),
+                  "0.5",
+                  "Animation value should have been filled in using computed " +
+                  "value.");
+    for (let keyframe of anim.effect.getKeyframes()) {
+      assert_true(keyframe["--number"] != undefined,
+        "Should have --number in keyframes, serialized as a custom property.");
+    }
+  } finally {
+    CSS.unregisterProperty("--number");
+    grandparent.style.removeProperty("animation-name");
+    grandparent.style.removeProperty("animation-duration");
+    grandparent.style.removeProperty("animation-timing-function");
+  }
+}, "Custom properties in missing keyframes using computed values should be " +
+   "filled in.");
+
+for (let spec of familyTests) {
+  test(function () {
+    let registered = [];
+    try {
+      for (let registration of spec.registrations) {
+        CSS.registerProperty(registration);
+        registered.push(registration.name);
+      }
+
+      grandparent.setAttribute("style", spec.grandparent || "");
+      parent.setAttribute("style", spec.parent || "");
+      child.setAttribute("style", spec.child || "");
+
+      for (let key in spec.expected) {
+        assert_equals(window.getComputedStyle(child).getPropertyValue(key),
+          spec.expected[key], "property value equals expected value");
+      }
+    } finally {
+      grandparent.removeAttribute("style");
+      parent.removeAttribute("style");
+      child.removeAttribute("style");
+
+      for (let name of registered) {
+        CSS.unregisterProperty(name);
+      }
+    }
+  }, "family: " + JSON.stringify(spec));
+}
+
+let expectFail = function (testName, specs, type, message) {
+  for (let spec of specs) {
+    test(function () {
+      if (!spec.name) {
+        spec.name = "--test-property";
+      }
+      let caught = null;
+      try {
+        CSS.registerProperty(spec);
+      } catch (e) {
+        caught = e;
+      }
+      let passed = caught instanceof DOMException && caught.name == type;
+      try {
+        assert_true(passed, message + " " + JSON.stringify(spec));
+      } finally {
+        if (!passed) {
+          CSS.unregisterProperty(spec.name);
+          throw caught;
+        }
+      }
+    }, testName + ": " + JSON.stringify(spec));
+  }
+};
+
+let expectSucceed = function (testName, specs, message) {
+  for (let spec of specs) {
+    test(function () {
+      if (!spec.name) {
+        spec.name = "--test-property";
+      }
+      let caught = null;
+      try {
+        CSS.registerProperty(spec);
+      } catch (e) {
+        caught = e;
+      }
+      let passed = !caught;
+      assert_true(passed, message + " " + JSON.stringify(spec));
+      if (passed) {
+        CSS.unregisterProperty(spec.name);
+      }
+    }, testName + ": " + JSON.stringify(spec));
+  }
+};
+
+expectFail("idempotent fail", idempotentFailTests, "SyntaxError",
+  "Initial values that are not computationally idempotent must trigger a " +
+  "SyntaxError.");
+
+expectSucceed("idempotent pass", idempotentPassTests,
+  "Initial values that are computationally idempotent must not trigger a " +
+  "SyntaxError.");
+
+expectFail("invalid syntax", invalidSyntaxTests, "SyntaxError",
+  "Invalid syntaxes must trigger a SyntaxError.");
+
+expectSucceed("valid syntax", validSyntaxTests,
+  "Valid syntaxes must not trigger a SyntaxError.");
+
+expectFail("syntax fail", syntaxFailTests, "SyntaxError",
+  "Initial values that are syntactically invalid must trigger a " +
+  "SyntaxError.");
+
+expectSucceed("syntax pass", syntaxPassTests,
+  "Initial values that are syntactically valid must not trigger a " +
+  "SyntaxError.");
+
+expectFail("invalid name", invalidNameTests, "SyntaxError",
+  "Attempting to register properties with a name that doesn’t correspond " +
+  "to the <custom-property-name> production must cause registerProperty() " +
+  "to throw a SyntaxError.");
+
+expectSucceed("valid name", validNameTests, "Allow valid custom property names.");
+
+// We should error on duplicate registrations.
+test(function () {
+  let caughtDuplicate = false;
+  CSS.registerProperty({ name: "--duplicate-ime" });
+  try {
+    CSS.registerProperty({ name: "--duplicate-ime" });
+    CSS.unregisterProperty("--duplicate-ime");
+  } catch (e) {
+    caughtDuplicate =
+    e instanceof DOMException &&
+      e.name == "InvalidModificationError";
+  }
+  assert_true(caughtDuplicate, "caught exception");
+}, "Attempting to register a property with a name that has already been " +
+   "registered should result in an InvalidModificationError.");
+
+// We should error when trying to unregister a property that hasn't been
+// registered.
+test(function () {
+  let caughtNotFound = false;
+  try {
+    CSS.unregisterProperty("--not-found-nfe");
+  } catch (e) {
+    caughtNotFound =
+      e instanceof DOMException &&
+      e.name == "NotFoundError";
+  }
+  assert_true(caughtNotFound, "caught exception");
+}, "Attempting to unregister a property that doesn't exist should result in " +
+   "a NotFoundError.");
+
+for (let spec of computedValueTests) {
+  let { syntax, declared, expected } = spec;
+  test(function () {
+    try {
+      CSS.registerProperty({ name: "--computed", syntax: syntax });
+      child.setAttribute("style", `--computed:${declared};`);
+
+      assert_equals(window.getComputedStyle(child).getPropertyValue("--computed"),
+                    expected, "computed value equals expected value");
+
+    } finally {
+      child.removeAttribute("style");
+      CSS.unregisterProperty("--computed");
+    }
+  }, "computed value: " + JSON.stringify(spec));
+}
+</script>