Bug 1260655 - Return the stored Keyframe objects from GetFrames, when available ; r=heycam
authorBrian Birtles <birtles@gmail.com>
Wed, 30 Mar 2016 08:59:08 +0900
changeset 292559 33b9f509fab7b3ee00a2738a5301c24880e3d82a
parent 292558 852340c877fb6cc98612ec43ddb14838068e2470
child 292560 51a410cc49c88ae03ca11760f8d914ef206188e4
push id74886
push userbbirtles@mozilla.com
push dateMon, 11 Apr 2016 05:50:31 +0000
treeherdermozilla-inbound@52c0128aff10 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersheycam
bugs1260655
milestone48.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1260655 - Return the stored Keyframe objects from GetFrames, when available ; r=heycam Before switching CSS animations over to using KeyframeEffectReadOnly::SetFrames we update the getFrames() API to return the set frame objects (when available) so that we can test that we are setting the correct frames. MozReview-Commit-ID: 4SpBRM7Ykyv
dom/animation/KeyframeEffect.cpp
testing/web-platform/tests/web-animations/keyframe-effect/constructor.html
--- a/dom/animation/KeyframeEffect.cpp
+++ b/dom/animation/KeyframeEffect.cpp
@@ -1105,21 +1105,90 @@ KeyframeEffectReadOnly::GetProperties(
         propertyDetails.mValues.AppendElement(toValue, mozilla::fallible);
       }
     }
 
     aProperties.AppendElement(propertyDetails);
   }
 }
 
+// TODO: This will eventually become the new GetFrames
+static void
+GetFramesFromFrames(JSContext*& aCx,
+                    const nsTArray<Keyframe>& aFrames,
+                    nsTArray<JSObject*>& aResult,
+                    ErrorResult& aRv)
+{
+  MOZ_ASSERT(aResult.IsEmpty());
+  MOZ_ASSERT(!aRv.Failed());
+
+  if (!aResult.SetCapacity(aFrames.Length(), mozilla::fallible)) {
+    aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+    return;
+  }
+
+  for (const Keyframe& keyframe : aFrames) {
+    // Set up a dictionary object for the explicit members
+    BaseComputedKeyframe keyframeDict;
+    if (keyframe.mOffset) {
+      keyframeDict.mOffset.SetValue(keyframe.mOffset.value());
+    }
+    keyframeDict.mComputedOffset.Construct(keyframe.mComputedOffset);
+    if (keyframe.mTimingFunction) {
+      keyframeDict.mEasing.Truncate();
+      keyframe.mTimingFunction.ref().AppendToString(keyframeDict.mEasing);
+    } // else if null, leave easing as its default "linear".
+
+    JS::Rooted<JS::Value> keyframeJSValue(aCx);
+    if (!ToJSValue(aCx, keyframeDict, &keyframeJSValue)) {
+      aRv.Throw(NS_ERROR_FAILURE);
+      return;
+    }
+
+    JS::Rooted<JSObject*> keyframeObject(aCx, &keyframeJSValue.toObject());
+    for (const PropertyValuePair& propertyValue : keyframe.mPropertyValues) {
+
+      const char* name = nsCSSProps::PropertyIDLName(propertyValue.mProperty);
+
+      // nsCSSValue::AppendToString does not accept shorthands properties but
+      // works with token stream values if we pass eCSSProperty_UNKNOWN as
+      // the property.
+      nsCSSProperty propertyForSerializing =
+        nsCSSProps::IsShorthand(propertyValue.mProperty)
+        ? eCSSProperty_UNKNOWN
+        : propertyValue.mProperty;
+
+      nsAutoString stringValue;
+      propertyValue.mValue.AppendToString(
+        propertyForSerializing, stringValue, nsCSSValue::eNormalized);
+
+      JS::Rooted<JS::Value> value(aCx);
+      if (!ToJSValue(aCx, stringValue, &value) ||
+          !JS_DefineProperty(aCx, keyframeObject, name, value,
+                             JSPROP_ENUMERATE)) {
+        aRv.Throw(NS_ERROR_FAILURE);
+        return;
+      }
+    }
+
+    aResult.AppendElement(keyframeObject);
+  }
+}
+
 void
 KeyframeEffectReadOnly::GetFrames(JSContext*& aCx,
                                   nsTArray<JSObject*>& aResult,
                                   ErrorResult& aRv)
 {
+  // Use the specified frames if we have any
+  if (!mFrames.IsEmpty()) {
+    GetFramesFromFrames(aCx, mFrames, aResult, aRv);
+    return;
+  }
+
   nsTArray<OrderedKeyframeValueEntry> entries;
 
   for (const AnimationProperty& property : mProperties) {
     for (size_t i = 0, n = property.mSegments.Length(); i < n; i++) {
       const AnimationPropertySegment& segment = property.mSegments[i];
 
       // We append the mFromValue for each segment.  If the mToValue
       // differs from the following segment's mFromValue, or if we're on
--- a/testing/web-platform/tests/web-animations/keyframe-effect/constructor.html
+++ b/testing/web-platform/tests/web-animations/keyframe-effect/constructor.html
@@ -33,27 +33,16 @@ function assert_frame_lists_equal(a, b) 
   assert_equals(a.length, b.length, "number of frames");
   for (var i = 0; i < Math.min(a.length, b.length); i++) {
     assert_frames_equal(a[i], b[i], "ComputedKeyframe #" + i);
   }
 }
 
 var gEmptyKeyframeListTests = [
   [],
-  [{}],
-  [{ easing: "ease-in" }],
-  [{ unknown: "unknown" }, { unknown: "unknown" }],
-  [{ color: "invalid" }, { color: "invalid" }],
-  { easing: "ease-in" },
-  { unknown: "unknown" },
-  { unknown: [] },
-  { unknown: ["unknown"] },
-  { unknown: ["unknown", "unknown"] },
-  { animationName: ["none", "abc"] },
-  { color: [] },
   null,
   undefined,
 ];
 
 test(function(t) {
   gEmptyKeyframeListTests.forEach(function(frames) {
     assert_equals(new KeyframeEffectReadOnly(target, frames).getFrames().length,
                   0, "number of frames for " + JSON.stringify(frames));
@@ -174,124 +163,126 @@ test(function(t) {
                                  });
   });
 }, "composite values are parsed correctly when passed to the " +
    "KeyframeEffectReadOnly constructor in KeyframeTimingOptions");
 
 var gPropertyIndexedKeyframesTests = [
   { desc:   "a one property two value property-indexed keyframes specification",
     input:  { left: ["10px", "20px"] },
-    output: [{ offset: 0, computedOffset: 0, easing: "linear", left: "10px" },
-             { offset: 1, computedOffset: 1, easing: "linear", left: "20px" }]
-  },
+    output: [{ offset: null, computedOffset: 0, easing: "linear",
+               left: "10px" },
+             { offset: null, computedOffset: 1, easing: "linear",
+               left: "20px" }] },
   { desc:   "a one shorthand property two value property-indexed keyframes"
             + " specification",
     input:  { margin: ["10px", "10px 20px 30px 40px"] },
-    output: [{ offset: 0, computedOffset: 0, easing: "linear",
-               marginTop: "10px", marginRight: "10px",
-               marginBottom: "10px", marginLeft: "10px" },
-             { offset: 1, computedOffset: 1, easing: "linear",
-               marginTop: "10px", marginRight: "20px",
-               marginBottom: "30px", marginLeft: "40px" }] },
+    output: [{ offset: null, computedOffset: 0, easing: "linear",
+               margin: "10px" },
+             { offset: null, computedOffset: 1, easing: "linear",
+               margin: "10px 20px 30px 40px" }] },
   { desc:   "a two property (one shorthand and one of its longhand components)"
             + " two value property-indexed keyframes specification",
     input:  { marginTop: ["50px", "60px"],
               margin: ["10px", "10px 20px 30px 40px"] },
-    output: [{ offset: 0, computedOffset: 0, easing: "linear",
-               marginTop: "50px", marginRight: "10px",
-               marginBottom: "10px", marginLeft: "10px" },
-             { offset: 1, computedOffset: 1, easing: "linear",
-               marginTop: "60px", marginRight: "20px",
-               marginBottom: "30px", marginLeft: "40px" }] },
+    output: [{ offset: null, computedOffset: 0, easing: "linear",
+               marginTop: "50px", margin: "10px" },
+             { offset: null, computedOffset: 1, easing: "linear",
+               marginTop: "60px", margin: "10px 20px 30px 40px" }] },
   { desc:   "a two property two value property-indexed keyframes specification",
     input:  { left: ["10px", "20px"],
               top: ["30px", "40px"] },
-    output: [{ offset: 0, computedOffset: 0, easing: "linear",
+    output: [{ offset: null, computedOffset: 0, easing: "linear",
                left: "10px", top: "30px" },
-             { offset: 1, computedOffset: 1, easing: "linear",
+             { offset: null, computedOffset: 1, easing: "linear",
                left: "20px", top: "40px" }] },
   { desc:   "a two property property-indexed keyframes specification with"
             + " different numbers of values",
     input:  { left: ["10px", "20px", "30px"],
               top: ["40px", "50px"] },
-    output: [{ offset: 0.0, computedOffset: 0.0, easing: "linear",
+    output: [{ offset: null, computedOffset: 0.0, easing: "linear",
                left: "10px", top: "40px" },
-             { offset: 0.5, computedOffset: 0.5, easing: "linear",
+             { offset: null, computedOffset: 0.5, easing: "linear",
                left: "20px" },
-             { offset: 1.0, computedOffset: 1.0, easing: "linear",
+             { offset: null, computedOffset: 1.0, easing: "linear",
                left: "30px", top: "50px" }] },
   { desc:   "a property-indexed keyframes specification with an invalid value",
     input:  { left: ["10px", "20px", "30px", "40px", "50px"],
               top:  ["15px", "25px", "invalid", "45px", "55px"] },
-    output: [{ offset: 0.00, computedOffset: 0.00, easing: "linear",
+    output: [{ offset: null, computedOffset: 0.00, easing: "linear",
                left: "10px", top: "15px" },
-             { offset: 0.25, computedOffset: 0.25, easing: "linear",
+             { offset: null, computedOffset: 0.25, easing: "linear",
                left: "20px", top: "25px" },
-             { offset: 0.50, computedOffset: 0.50, easing: "linear",
-               left: "30px" },
-             { offset: 0.75, computedOffset: 0.75, easing: "linear",
+             { offset: null, computedOffset: 0.50, easing: "linear",
+               left: "30px", top: "invalid" },
+             { offset: null, computedOffset: 0.75, easing: "linear",
                left: "40px", top: "45px" },
-             { offset: 1.00, computedOffset: 1.00, easing: "linear",
+             { offset: null, computedOffset: 1.00, easing: "linear",
                left: "50px", top: "55px" }] },
   { desc:   "a one property two value property-indexed keyframes specification"
             + " that needs to stringify its values",
     input:  { opacity: [0, 1] },
-    output: [{ offset: 0, computedOffset: 0, easing: "linear", opacity: "0" },
-             { offset: 1, computedOffset: 1, easing: "linear", opacity: "1" }]
-  },
+    output: [{ offset: null, computedOffset: 0, easing: "linear",
+               opacity: "0" },
+             { offset: null, computedOffset: 1, easing: "linear",
+               opacity: "1" }] },
   { desc:   "a one property one value property-indexed keyframes specification",
     input:  { left: ["10px"] },
-    output: [{ offset: 0, computedOffset: 0, easing: "linear" },
-             { offset: 1, computedOffset: 1, easing: "linear", left: "10px" }]
-  },
+    output: [{ offset: null, computedOffset: 1, easing: "linear",
+               left: "10px" }] },
   { desc:   "a one property one non-array value property-indexed keyframes"
             + " specification",
     input:  { left: "10px" },
-    output: [{ offset: 0, computedOffset: 0, easing: "linear" },
-             { offset: 1, computedOffset: 1, easing: "linear", left: "10px" }] },
+    output: [{ offset: null, computedOffset: 1, easing: "linear",
+               left: "10px" }] },
   { desc:   "a one property two value property-indexed keyframes specification"
             + " where the first value is invalid",
     input:  { left: ["invalid", "10px"] },
-    output: [{ offset: 0, computedOffset: 0, easing: "linear" },
-             { offset: 1, computedOffset: 1, easing: "linear", left: "10px" }] },
+    output: [{ offset: null, computedOffset: 0, easing: "linear",
+               left: "invalid" },
+             { offset: null, computedOffset: 1, easing: "linear",
+               left: "10px" }] },
   { desc:   "a one property two value property-indexed keyframes specification"
             + " where the second value is invalid",
     input:  { left: ["10px", "invalid"] },
-    output: [{ offset: 0, computedOffset: 0, easing: "linear", left: "10px" },
-             { offset: 1, computedOffset: 1, easing: "linear" }] },
+    output: [{ offset: null, computedOffset: 0, easing: "linear",
+               left: "10px" },
+             { offset: null, computedOffset: 1, easing: "linear",
+               left: "invalid" }] },
   { desc:   "a two property property-indexed keyframes specification where one"
             + " property is missing from the first keyframe",
     input:  [{ offset: 0, left: "10px" },
              { offset: 1, left: "20px", top: "30px" }],
     output: [{ offset: 0, computedOffset: 0, easing: "linear", left: "10px" },
              { offset: 1, computedOffset: 1, easing: "linear",
                left: "20px", top: "30px" }] },
   { desc:   "a two property property-indexed keyframes specification where one"
             + " property is missing from the last keyframe",
     input:  [{ offset: 0, left: "10px", top: "20px" },
              { offset: 1, left: "30px" }],
     output: [{ offset: 0, computedOffset: 0, easing: "linear",
                left: "10px" , top: "20px" },
-             { offset: 1, computedOffset: 1, easing: "linear", left: "30px" }] },
+             { offset: 1, computedOffset: 1, easing: "linear",
+               left: "30px" }] },
   { desc:   "a property-indexed keyframes specification with repeated values"
             + " at offset 0 with different easings",
     input:  [{ offset: 0.0, left: "100px", easing: "ease" },
              { offset: 0.0, left: "200px", easing: "ease" },
              { offset: 0.5, left: "300px", easing: "linear" },
              { offset: 1.0, left: "400px", easing: "ease-out" },
              { offset: 1.0, left: "500px", easing: "step-end" }],
     output: [{ offset: 0.0, computedOffset: 0.0, easing: "ease",
                left: "100px" },
              { offset: 0.0, computedOffset: 0.0, easing: "ease",
                left: "200px" },
              { offset: 0.5, computedOffset: 0.5, easing: "linear",
                left: "300px" },
              { offset: 1.0, computedOffset: 1.0, easing: "ease-out",
                left: "400px" },
-             { offset: 1.0, computedOffset: 1.0, easing: "linear",
+             { offset: 1.0, computedOffset: 1.0, easing: "step-end",
                left: "500px" }] },
 ];
 
 gPropertyIndexedKeyframesTests.forEach(function(subtest) {
   test(function(t) {
     var effect = new KeyframeEffectReadOnly(target, subtest.input);
     assert_frame_lists_equal(effect.getFrames(), subtest.output);
   }, "a KeyframeEffectReadOnly can be constructed with " + subtest.desc);
@@ -337,199 +328,198 @@ var gKeyframeSequenceTests = [
     output: [{ offset: 0, computedOffset: 0, easing: "linear",
                left: "10px", top: "30px" },
              { offset: 1, computedOffset: 1, easing: "linear",
                left: "20px", top: "40px" }] },
   { desc:   "a one shorthand property two keyframe sequence",
     input:  [{ offset: 0, margin: "10px" },
              { offset: 1, margin: "20px 30px 40px 50px" }],
     output: [{ offset: 0, computedOffset: 0, easing: "linear",
-               marginTop: "10px", marginRight: "10px",
-               marginBottom: "10px", marginLeft: "10px" },
+               margin: "10px" },
              { offset: 1, computedOffset: 1, easing: "linear",
-               marginTop: "20px", marginRight: "30px",
-               marginBottom: "40px", marginLeft: "50px" }] },
+               margin: "20px 30px 40px 50px" }] },
   { desc:   "a two property (a shorthand and one of its component longhands)"
             + " two keyframe sequence",
     input:  [{ offset: 0, margin: "10px", marginTop: "20px" },
              { offset: 1, marginTop: "70px", margin: "30px 40px 50px 60px" }],
     output: [{ offset: 0, computedOffset: 0, easing: "linear",
-               marginTop: "20px", marginRight: "10px",
-               marginBottom: "10px", marginLeft: "10px" },
+               margin: "10px", marginTop: "20px" },
              { offset: 1, computedOffset: 1, easing: "linear",
-               marginTop: "70px", marginRight: "40px",
-               marginBottom: "50px", marginLeft: "60px" }] },
+               marginTop: "70px", margin: "30px 40px 50px 60px" }] },
   { desc:   "a keyframe sequence with duplicate values for a given interior"
             + " offset",
     input:  [{ offset: 0.0, left: "10px" },
              { offset: 0.5, left: "20px" },
              { offset: 0.5, left: "30px" },
              { offset: 0.5, left: "40px" },
              { offset: 1.0, left: "50px" }],
     output: [{ offset: 0.0, computedOffset: 0.0, easing: "linear",
                left: "10px" },
              { offset: 0.5, computedOffset: 0.5, easing: "linear",
                left: "20px" },
              { offset: 0.5, computedOffset: 0.5, easing: "linear",
+               left: "30px" },
+             { offset: 0.5, computedOffset: 0.5, easing: "linear",
                left: "40px" },
              { offset: 1.0, computedOffset: 1.0, easing: "linear",
                left: "50px" }] },
   { desc:   "a keyframe sequence with duplicate values for offsets 0 and 1",
     input:  [{ offset: 0, left: "10px" },
              { offset: 0, left: "20px" },
              { offset: 0, left: "30px" },
              { offset: 1, left: "40px" },
              { offset: 1, left: "50px" },
              { offset: 1, left: "60px" }],
     output: [{ offset: 0, computedOffset: 0, easing: "linear", left: "10px" },
+             { offset: 0, computedOffset: 0, easing: "linear", left: "20px" },
              { offset: 0, computedOffset: 0, easing: "linear", left: "30px" },
              { offset: 1, computedOffset: 1, easing: "linear", left: "40px" },
+             { offset: 1, computedOffset: 1, easing: "linear", left: "50px" },
              { offset: 1, computedOffset: 1, easing: "linear", left: "60px" }]
   },
   { desc:   "a two property four keyframe sequence",
     input:  [{ offset: 0, left: "10px" },
              { offset: 0, top: "20px" },
              { offset: 1, top: "30px" },
              { offset: 1, left: "40px" }],
-    output: [{ offset: 0, computedOffset: 0, easing: "linear",
-               left: "10px", top: "20px" },
-             { offset: 1, computedOffset: 1, easing: "linear",
-               left: "40px", top: "30px" }] },
+    output: [{ offset: 0, computedOffset: 0, easing: "linear", left: "10px" },
+             { offset: 0, computedOffset: 0, easing: "linear", top: "20px" },
+             { offset: 1, computedOffset: 1, easing: "linear", top: "30px" },
+             { offset: 1, computedOffset: 1, easing: "linear", left: "40px" }]
+  },
   { desc:   "a one property keyframe sequence with some omitted offsets",
     input:  [{ offset: 0.00, left: "10px" },
              { offset: 0.25, left: "20px" },
              { left: "30px" },
              { left: "40px" },
              { offset: 1.00, left: "50px" }],
     output: [{ offset: 0.00, computedOffset: 0.00, easing: "linear",
                left: "10px" },
              { offset: 0.25, computedOffset: 0.25, easing: "linear",
                left: "20px" },
-             { offset: 0.50, computedOffset: 0.50, easing: "linear",
+             { offset: null, computedOffset: 0.50, easing: "linear",
                left: "30px" },
-             { offset: 0.75, computedOffset: 0.75, easing: "linear",
+             { offset: null, computedOffset: 0.75, easing: "linear",
                left: "40px" },
              { offset: 1.00, computedOffset: 1.00, easing: "linear",
                left: "50px" }] },
   { desc:   "a two property keyframe sequence with some omitted offsets",
     input:  [{ offset: 0.00, left: "10px", top: "20px" },
              { offset: 0.25, left: "30px" },
              { left: "40px" },
              { left: "50px", top: "60px" },
              { offset: 1.00, left: "70px", top: "80px" }],
     output: [{ offset: 0.00, computedOffset: 0.00, easing: "linear",
                left: "10px", top: "20px" },
              { offset: 0.25, computedOffset: 0.25, easing: "linear",
                left: "30px" },
-             { offset: 0.50, computedOffset: 0.50, easing: "linear",
+             { offset: null, computedOffset: 0.50, easing: "linear",
                left: "40px" },
-             { offset: 0.75, computedOffset: 0.75, easing: "linear",
+             { offset: null, computedOffset: 0.75, easing: "linear",
                left: "50px", top: "60px" },
              { offset: 1.00, computedOffset: 1.00, easing: "linear",
                left: "70px", top: "80px" }] },
   { desc:   "a one property keyframe sequence with all omitted offsets",
     input:  [{ left: "10px" },
              { left: "20px" },
              { left: "30px" },
              { left: "40px" },
              { left: "50px" }],
-    output: [{ offset: 0.00, computedOffset: 0.00, easing: "linear",
+    output: [{ offset: null, computedOffset: 0.00, easing: "linear",
                left: "10px" },
-             { offset: 0.25, computedOffset: 0.25, easing: "linear",
+             { offset: null, computedOffset: 0.25, easing: "linear",
                left: "20px" },
-             { offset: 0.50, computedOffset: 0.50, easing: "linear",
+             { offset: null, computedOffset: 0.50, easing: "linear",
                left: "30px" },
-             { offset: 0.75, computedOffset: 0.75, easing: "linear",
+             { offset: null, computedOffset: 0.75, easing: "linear",
                left: "40px" },
-             { offset: 1.00, computedOffset: 1.00, easing: "linear",
+             { offset: null, computedOffset: 1.00, easing: "linear",
                left: "50px" }] },
   { desc:   "a keyframe sequence with different easing values, but the same"
             + " easing value for a given offset",
     input:  [{ offset: 0.0, easing: "ease",     left: "10px"},
              { offset: 0.0, easing: "ease",     top: "20px"},
              { offset: 0.5, easing: "linear",   left: "30px" },
              { offset: 0.5, easing: "linear",   top: "40px" },
              { offset: 1.0, easing: "step-end", left: "50px" },
              { offset: 1.0, easing: "step-end", top: "60px" }],
     output: [{ offset: 0.0, computedOffset: 0.0, easing: "ease",
-               left: "10px", top: "20px" },
+               left: "10px" },
+             { offset: 0.0, computedOffset: 0.0, easing: "ease",
+               top: "20px" },
+             { offset: 0.5, computedOffset: 0.5, easing: "linear",
+               left: "30px" },
              { offset: 0.5, computedOffset: 0.5, easing: "linear",
-               left: "30px", top: "40px" },
-             { offset: 1.0, computedOffset: 1.0, easing: "linear",
-               left: "50px", top: "60px" }] },
+               top: "40px" },
+             { offset: 1.0, computedOffset: 1.0, easing: "step-end",
+               left: "50px" },
+             { offset: 1.0, computedOffset: 1.0, easing: "step-end",
+               top: "60px" }] },
   { desc:   "a keyframe sequence with different composite values, but the"
             + " same composite value for a given offset",
     input:  [{ offset: 0.0, composite: "replace", left: "10px" },
              { offset: 0.0, composite: "replace", top: "20px" },
              { offset: 0.5, composite: "add",     left: "30px" },
              { offset: 0.5, composite: "add",     top: "40px" },
              { offset: 1.0, composite: "replace", left: "50px" },
              { offset: 1.0, composite: "replace", top: "60px" }],
     output: [{ offset: 0.0, computedOffset: 0.0, easing: "linear",
-               composite: "replace", left: "10px", top: "20px" },
-             { offset: 0.5, computedOffset: 0.5, easing: "linear",
-               composite: "add",     left: "30px", top: "40px" },
+               composite: "replace", left: "10px" },
+             { offset: 0.0, computedOffset: 0.0, easing: "linear",
+               composite: "replace", top: "20px" },
+             { offset: 0.5, computedOffset: 0.0, easing: "linear",
+               composite: "add", left: "30px" },
+             { offset: 0.5, computedOffset: 0.0, easing: "linear",
+               composite: "add", top: "40px" },
              { offset: 1.0, computedOffset: 1.0, easing: "linear",
-               composite: "replace", left: "50px", top: "60px" }] },
+               composite: "replace", left: "50px" },
+             { offset: 1.0, computedOffset: 1.0, easing: "linear",
+               composite: "replace", top: "60px" }] },
   { desc:   "a one property two keyframe sequence that needs to stringify"
             + " its values",
     input:  [{ offset: 0, opacity: 0 },
              { offset: 1, opacity: 1 }],
     output: [{ offset: 0, computedOffset: 0, easing: "linear", opacity: "0" },
              { offset: 1, computedOffset: 1, easing: "linear", opacity: "1" }]
   },
   { desc:   "a keyframe sequence where shorthand precedes longhand",
     input:  [{ offset: 0, margin: "10px", marginRight: "20px" },
              { offset: 1, margin: "30px" }],
     output: [{ offset: 0, computedOffset: 0, easing: "linear",
-               marginBottom: "10px", marginLeft: "10px",
-               marginRight: "20px", marginTop: "10px" },
+               margin: "10px", marginRight: "20px" },
              { offset: 1, computedOffset: 1, easing: "linear",
-               marginBottom: "30px", marginLeft: "30px",
-               marginRight: "30px", marginTop: "30px" }] },
+               margin: "30px" }] },
   { desc:   "a keyframe sequence where longhand precedes shorthand",
     input:  [{ offset: 0, marginRight: "20px", margin: "10px" },
              { offset: 1, margin: "30px" }],
     output: [{ offset: 0, computedOffset: 0, easing: "linear",
-               marginBottom: "10px", marginLeft: "10px",
-               marginRight: "20px", marginTop: "10px" },
+               marginRight: "20px", margin: "10px" },
              { offset: 1, computedOffset: 1, easing: "linear",
-               marginBottom: "30px", marginLeft: "30px",
-               marginRight: "30px", marginTop: "30px" }] },
+               margin: "30px" }] },
   { desc:   "a keyframe sequence where lesser shorthand precedes greater"
             + " shorthand",
     input:  [{ offset: 0,
                borderLeft: "1px solid rgb(1, 2, 3)",
                border: "2px dotted rgb(4, 5, 6)" },
              { offset: 1, border: "3px dashed rgb(7, 8, 9)" }],
     output: [{ offset: 0, computedOffset: 0, easing: "linear",
-               borderBottomColor: "rgb(4, 5, 6)", borderBottomWidth: "2px",
-               borderLeftColor:   "rgb(1, 2, 3)", borderLeftWidth:   "1px",
-               borderRightColor:  "rgb(4, 5, 6)", borderRightWidth:  "2px",
-               borderTopColor:    "rgb(4, 5, 6)", borderTopWidth:    "2px" },
+               borderLeft: "1px solid rgb(1, 2, 3)",
+               border: "2px dotted rgb(4, 5, 6)" },
              { offset: 1, computedOffset: 1, easing: "linear",
-               borderBottomColor: "rgb(7, 8, 9)", borderBottomWidth: "3px",
-               borderLeftColor:   "rgb(7, 8, 9)", borderLeftWidth:   "3px",
-               borderRightColor:  "rgb(7, 8, 9)", borderRightWidth:  "3px",
-               borderTopColor:    "rgb(7, 8, 9)", borderTopWidth:    "3px" }] },
+               border: "3px dashed rgb(7, 8, 9)" }] },
   { desc:   "a keyframe sequence where greater shorthand precedes lesser"
             + " shorthand",
     input:  [{ offset: 0, border: "2px dotted rgb(4, 5, 6)",
                borderLeft: "1px solid rgb(1, 2, 3)" },
              { offset: 1, border: "3px dashed rgb(7, 8, 9)" }],
     output: [{ offset: 0, computedOffset: 0, easing: "linear",
-               borderBottomColor: "rgb(4, 5, 6)", borderBottomWidth: "2px",
-               borderLeftColor:   "rgb(1, 2, 3)", borderLeftWidth:   "1px",
-               borderRightColor:  "rgb(4, 5, 6)", borderRightWidth:  "2px",
-               borderTopColor:    "rgb(4, 5, 6)", borderTopWidth:    "2px" },
+               border: "2px dotted rgb(4, 5, 6)",
+               borderLeft: "1px solid rgb(1, 2, 3)" },
              { offset: 1, computedOffset: 1, easing: "linear",
-               borderBottomColor: "rgb(7, 8, 9)", borderBottomWidth: "3px",
-               borderLeftColor:   "rgb(7, 8, 9)", borderLeftWidth:   "3px",
-               borderRightColor:  "rgb(7, 8, 9)", borderRightWidth:  "3px",
-               borderTopColor:    "rgb(7, 8, 9)", borderTopWidth:    "3px" }] },
+               border: "3px dashed rgb(7, 8, 9)" }] },
 ];
 
 gKeyframeSequenceTests.forEach(function(subtest) {
   test(function(t) {
     var effect = new KeyframeEffectReadOnly(target, subtest.input);
     assert_frame_lists_equal(effect.getFrames(), subtest.output);
   }, "a KeyframeEffectReadOnly can be constructed with " + subtest.desc);