Bug 1429671 - Make composite member of Keyframe dictionary objects accept null values; r=bz
authorBrian Birtles <birtles@gmail.com>
Thu, 11 Jan 2018 16:20:49 +0900
changeset 450694 91211b85310ea86878929af0a97906b6fc60781a
parent 450693 10c18fb37dc9223561d5852e4d198f055c7f0df5
child 450695 93891a8e16e12ea81fcc8f08b670bbd7695298ba
push id8531
push userryanvm@gmail.com
push dateFri, 12 Jan 2018 16:47:01 +0000
treeherdermozilla-beta@0bc627ade5a0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbz
bugs1429671
milestone59.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 1429671 - Make composite member of Keyframe dictionary objects accept null values; r=bz This patch reflects the following change to the Web Animations spec: https://github.com/w3c/csswg-drafts/commit/abf76745b50c8943e8b16c13abc61cb6ca814830 MozReview-Commit-ID: A2GD1igUf5f
dom/animation/KeyframeEffectReadOnly.cpp
dom/animation/KeyframeUtils.cpp
dom/animation/test/css-animations/file_keyframeeffect-getkeyframes.html
dom/animation/test/css-transitions/file_keyframeeffect-getkeyframes.html
dom/webidl/BaseKeyframeTypes.webidl
testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/composite.html
testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/constructor.html
testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/processing-a-keyframes-argument-001.html
testing/web-platform/tests/web-animations/resources/keyframe-tests.js
--- a/dom/animation/KeyframeEffectReadOnly.cpp
+++ b/dom/animation/KeyframeEffectReadOnly.cpp
@@ -1283,17 +1283,17 @@ KeyframeEffectReadOnly::GetKeyframes(JSC
                "Invalid computed offset");
     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".
 
     if (keyframe.mComposite) {
-      keyframeDict.mComposite.Construct(keyframe.mComposite.value());
+      keyframeDict.mComposite.SetValue(keyframe.mComposite.value());
     }
 
     JS::Rooted<JS::Value> keyframeJSValue(aCx);
     if (!ToJSValue(aCx, keyframeDict, &keyframeJSValue)) {
       aRv.Throw(NS_ERROR_FAILURE);
       return;
     }
 
--- a/dom/animation/KeyframeUtils.cpp
+++ b/dom/animation/KeyframeUtils.cpp
@@ -653,17 +653,17 @@ ConvertKeyframeSequence(JSContext* aCx,
     Keyframe* keyframe = aResult.AppendElement(fallible);
     if (!keyframe) {
       return false;
     }
     if (!keyframeDict.mOffset.IsNull()) {
       keyframe->mOffset.emplace(keyframeDict.mOffset.Value());
     }
 
-    if (keyframeDict.mComposite.WasPassed()) {
+    if (!keyframeDict.mComposite.IsNull()) {
       keyframe->mComposite.emplace(keyframeDict.mComposite.Value());
     }
 
     // Look for additional property-values pairs on the object.
     nsTArray<PropertyValuesPair> propertyValuePairs;
     if (value.isObject()) {
       JS::Rooted<JSObject*> object(aCx, &value.toObject());
       if (!GetPropertyValuesPairs(aCx, object,
@@ -1542,33 +1542,37 @@ GetKeyframeListFromPropertyIndexedKeyfra
       aResult[i].mTimingFunction = easings[i % easings.Length()];
     }
   }
 
   // Fill in any composite operations.
   //
   // This corresponds to step 5, "Otherwise," branch, substep 12 of
   // https://drafts.csswg.org/web-animations/#processing-a-keyframes-argument
-  const FallibleTArray<dom::CompositeOperation>* compositeOps;
-  AutoTArray<dom::CompositeOperation, 1> singleCompositeOp;
+  const FallibleTArray<Nullable<dom::CompositeOperation>>* compositeOps =
+    nullptr;
+  AutoTArray<Nullable<dom::CompositeOperation>, 1> singleCompositeOp;
   auto& composite = keyframeDict.mComposite;
   if (composite.IsCompositeOperation()) {
     singleCompositeOp.AppendElement(composite.GetAsCompositeOperation());
-    const FallibleTArray<dom::CompositeOperation>& asFallibleArray =
+    const FallibleTArray<Nullable<dom::CompositeOperation>>& asFallibleArray =
       singleCompositeOp;
     compositeOps = &asFallibleArray;
-  } else {
-    compositeOps = &composite.GetAsCompositeOperationSequence();
+  } else if (composite.IsCompositeOperationOrNullSequence()) {
+    compositeOps = &composite.GetAsCompositeOperationOrNullSequence();
   }
 
   // Fill in and repeat as needed.
-  if (!compositeOps->IsEmpty()) {
+  if (compositeOps && !compositeOps->IsEmpty()) {
+    size_t length = compositeOps->Length();
     for (size_t i = 0; i < aResult.Length(); i++) {
-      aResult[i].mComposite.emplace(
-        compositeOps->ElementAt(i % compositeOps->Length()));
+      if (!compositeOps->ElementAt(i % length).IsNull()) {
+        aResult[i].mComposite.emplace(
+          compositeOps->ElementAt(i % length).Value());
+      }
     }
   }
 }
 
 /**
  * Returns true if the supplied set of keyframes has keyframe values for
  * any property for which it does not also supply a value for the 0% and 100%
  * offsets. In this case we are supposed to synthesize an additive zero value
--- a/dom/animation/test/css-animations/file_keyframeeffect-getkeyframes.html
+++ b/dom/animation/test/css-animations/file_keyframeeffect-getkeyframes.html
@@ -220,26 +220,26 @@ test(function(t) {
 
   div.style.animation = 'anim-simple 100s';
   var frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   var expected = [
     { offset: 0, computedOffset: 0, easing: "ease",
-      color: "rgb(0, 0, 0)" },
+      color: "rgb(0, 0, 0)", composite: null },
     { offset: 1, computedOffset: 1, easing: "ease",
-      color: "rgb(255, 255, 255)" },
+      color: "rgb(255, 255, 255)", composite: null },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for a simple'
-   + 'animation');
+   + ' animation');
 
 test(function(t) {
   kTimingFunctionValues.forEach(function(easing) {
     var div = addDiv(t);
 
     div.style.animation = 'anim-simple-three 100s ' + easing;
     var frames = getKeyframes(div);
 
@@ -290,20 +290,20 @@ test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-simple-shorthand 100s';
   var frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   var expected = [
-    { offset: 0, computedOffset: 0, easing: "ease",
+    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
       marginBottom: "8px", marginLeft: "8px",
       marginRight: "8px", marginTop: "8px" },
-    { offset: 1, computedOffset: 1, easing: "ease",
+    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       marginBottom: "16px", marginLeft: "16px",
       marginRight: "16px", marginTop: "16px" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for a simple'
@@ -314,19 +314,19 @@ test(function(t) {
 
   div.style.animation = 'anim-omit-to 100s';
   div.style.color = 'rgb(255, 255, 255)';
   var frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   var expected = [
-    { offset: 0, computedOffset: 0, easing: "ease",
+    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
       color: "rgb(0, 0, 255)" },
-    { offset: 1, computedOffset: 1, easing: "ease",
+    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       color: "rgb(255, 255, 255)" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
    'animation with a 0% keyframe and no 100% keyframe');
@@ -336,19 +336,19 @@ test(function(t) {
 
   div.style.animation = 'anim-omit-from 100s';
   div.style.color = 'rgb(255, 255, 255)';
   var frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   var expected = [
-    { offset: 0, computedOffset: 0, easing: "ease",
+    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
       color: "rgb(255, 255, 255)" },
-    { offset: 1, computedOffset: 1, easing: "ease",
+    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       color: "rgb(0, 0, 255)" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
    'animation with a 100% keyframe and no 0% keyframe');
@@ -358,21 +358,21 @@ test(function(t) {
 
   div.style.animation = 'anim-omit-from-to 100s';
   div.style.color = 'rgb(255, 255, 255)';
   var frames = getKeyframes(div);
 
   assert_equals(frames.length, 3, "number of frames");
 
   var expected = [
-    { offset: 0,   computedOffset: 0,   easing: "ease",
+    { offset: 0,   computedOffset: 0,   easing: "ease", composite: null,
       color: "rgb(255, 255, 255)" },
-    { offset: 0.5, computedOffset: 0.5, easing: "ease",
+    { offset: 0.5, computedOffset: 0.5, easing: "ease", composite: null,
       color: "rgb(0, 0, 255)" },
-    { offset: 1,   computedOffset: 1,   easing: "ease",
+    { offset: 1,   computedOffset: 1,   easing: "ease", composite: null,
       color: "rgb(255, 255, 255)" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
    'animation with no 0% or 100% keyframe but with a 50% keyframe');
@@ -382,19 +382,19 @@ test(function(t) {
 
   div.style.animation = 'anim-partially-omit-to 100s';
   div.style.marginTop = '250px';
   var frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   var expected = [
-    { offset: 0, computedOffset: 0, easing: "ease",
+    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
       marginTop: '50px', marginBottom: '100px' },
-    { offset: 1, computedOffset: 1, easing: "ease",
+    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       marginTop: '250px', marginBottom: '200px' },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
    'animation with a partially complete 100% keyframe (because the ' +
@@ -404,23 +404,23 @@ test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-different-props 100s';
   var frames = getKeyframes(div);
 
   assert_equals(frames.length, 4, "number of frames");
 
   var expected = [
-    { offset: 0, computedOffset: 0, easing: "ease",
+    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
       color: "rgb(0, 0, 0)", marginTop: "8px" },
-    { offset: 0.25, computedOffset: 0.25, easing: "ease",
+    { offset: 0.25, computedOffset: 0.25, easing: "ease", composite: null,
       color: "rgb(0, 0, 255)" },
-    { offset: 0.75, computedOffset: 0.75, easing: "ease",
+    { offset: 0.75, computedOffset: 0.75, easing: "ease", composite: null,
       marginTop: "12px" },
-    { offset: 1, computedOffset: 1, easing: "ease",
+    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       color: "rgb(255, 255, 255)", marginTop: "16px" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
    'animation with different properties on different keyframes, all ' +
@@ -430,23 +430,23 @@ test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-different-props-and-easing 100s';
   var frames = getKeyframes(div);
 
   assert_equals(frames.length, 4, "number of frames");
 
   var expected = [
-    { offset: 0, computedOffset: 0, easing: "linear",
+    { offset: 0, computedOffset: 0, easing: "linear", composite: null,
       color: "rgb(0, 0, 0)", marginTop: "8px" },
-    { offset: 0.25, computedOffset: 0.25, easing: "steps(1)",
+    { offset: 0.25, computedOffset: 0.25, easing: "steps(1)", composite: null,
       color: "rgb(0, 0, 255)" },
-    { offset: 0.75, computedOffset: 0.75, easing: "ease-in",
+    { offset: 0.75, computedOffset: 0.75, easing: "ease-in", composite: null,
       marginTop: "12px" },
-    { offset: 1, computedOffset: 1, easing: "ease",
+    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       color: "rgb(255, 255, 255)", marginTop: "16px" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
    'animation with different properties on different keyframes, with ' +
@@ -456,19 +456,19 @@ test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-merge-offset 100s';
   var frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   var expected = [
-    { offset: 0, computedOffset: 0, easing: "ease",
+    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
       color: "rgb(0, 0, 0)", marginTop: "8px" },
-    { offset: 1, computedOffset: 1, easing: "ease",
+    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       color: "rgb(255, 255, 255)", marginTop: "16px" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
    'animation with multiple keyframes for the same time, and all with ' +
@@ -478,21 +478,21 @@ test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-merge-offset-and-easing 100s';
   var frames = getKeyframes(div);
 
   assert_equals(frames.length, 3, "number of frames");
 
   var expected = [
-    { offset: 0, computedOffset: 0, easing: "steps(1)",
+    { offset: 0, computedOffset: 0, easing: "steps(1)", composite: null,
       color: "rgb(0, 0, 0)", fontSize: "16px" },
-    { offset: 0, computedOffset: 0, easing: "linear",
+    { offset: 0, computedOffset: 0, easing: "linear", composite: null,
       marginTop: "8px", paddingLeft: "2px" },
-    { offset: 1, computedOffset: 1, easing: "ease",
+    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       color: "rgb(255, 255, 255)", fontSize: "32px", marginTop: "16px",
       paddingLeft: "4px" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
@@ -503,21 +503,21 @@ test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-no-merge-equiv-easing 100s';
   var frames = getKeyframes(div);
 
   assert_equals(frames.length, 3, "number of frames");
 
   var expected = [
-    { offset: 0, computedOffset: 0, easing: "steps(1)",
+    { offset: 0, computedOffset: 0, easing: "steps(1)", composite: null,
       marginTop: "0px", marginRight: "0px", marginBottom: "0px" },
-    { offset: 0.5, computedOffset: 0.5, easing: "steps(1)",
+    { offset: 0.5, computedOffset: 0.5, easing: "steps(1)", composite: null,
       marginTop: "10px", marginRight: "10px", marginBottom: "10px" },
-    { offset: 1, computedOffset: 1, easing: "ease",
+    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       marginTop: "20px", marginRight: "20px", marginBottom: "20px" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for an ' +
    'animation with multiple keyframes for the same time and with ' +
@@ -527,27 +527,27 @@ test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-overriding 100s';
   var frames = getKeyframes(div);
 
   assert_equals(frames.length, 6, "number of frames");
 
   var expected = [
-    { offset: 0, computedOffset: 0, easing: "ease",
+    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
       paddingTop: "30px" },
-    { offset: 0.5, computedOffset: 0.5, easing: "ease",
+    { offset: 0.5, computedOffset: 0.5, easing: "ease", composite: null,
       paddingTop: "20px" },
-    { offset: 0.75, computedOffset: 0.75, easing: "ease",
+    { offset: 0.75, computedOffset: 0.75, easing: "ease", composite: null,
       paddingTop: "20px" },
-    { offset: 0.85, computedOffset: 0.85, easing: "ease",
+    { offset: 0.85, computedOffset: 0.85, easing: "ease", composite: null,
       paddingTop: "30px" },
-    { offset: 0.851, computedOffset: 0.851, easing: "ease",
+    { offset: 0.851, computedOffset: 0.851, easing: "ease", composite: null,
       paddingTop: "60px" },
-    { offset: 1, computedOffset: 1, easing: "ease",
+    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       paddingTop: "70px" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for ' +
    'overlapping keyframes');
@@ -559,19 +559,19 @@ test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-filter 100s';
   var frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   var expected = [
-    { offset: 0, computedOffset: 0, easing: "ease",
+    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
       filter: "none" },
-    { offset: 1, computedOffset: 1, easing: "ease",
+    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       filter: "blur(5px) sepia(60%) saturate(30%)" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
    'animations with filter properties and missing keyframes');
@@ -580,19 +580,19 @@ test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-filter-drop-shadow 100s';
   var frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   var expected = [
-    { offset: 0, computedOffset: 0, easing: "ease",
+    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
       filter: "drop-shadow(rgb(0, 255, 0) 10px 10px 10px)" },
-    { offset: 1, computedOffset: 1, easing: "ease",
+    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       filter: "drop-shadow(rgb(255, 0, 0) 50px 30px 10px)" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
    'animation with drop-shadow of filter property');
@@ -608,21 +608,22 @@ test(function(t) {
                          '0 0 16px rgb(0, 0, 255), ' +
                          '0 0 3.2px rgb(0, 0, 255)';
   div.style.animation = 'anim-text-shadow 100s';
   var frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   var expected = [
-    { offset: 0, computedOffset: 0, easing: "ease",
+    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
       textShadow: "rgb(0, 0, 0) 1px 1px 2px,"
                   + " rgb(0, 0, 255) 0px 0px 16px,"
                   + " rgb(0, 0, 255) 0px 0px 3.2px" },
-    { offset: 1, computedOffset: 1, easing: "ease", textShadow: "none" },
+    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
+      textShadow: "none" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
    'animations with text-shadow properties and missing keyframes');
 
@@ -634,19 +635,19 @@ test(function(t) {
   var div = addDiv(t);
 
   div.style.animation = 'anim-background-size 100s';
   var frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   var expected = [
-    { offset: 0, computedOffset: 0, easing: "ease",
+    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
       backgroundSize: "auto auto" },
-    { offset: 1, computedOffset: 1, easing: "ease",
+    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       backgroundSize: "50% auto, 6px auto, contain" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 
   // Test inheriting a background-size value
@@ -666,19 +667,19 @@ test(function(t) {
   var div = addDiv(t);
   div.style.animation = 'anim-variables 100s';
 
   var frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   var expected = [
-    { offset: 0, computedOffset: 0, easing: "ease",
+    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
       transform: "none" },
-    { offset: 1, computedOffset: 1, easing: "ease",
+    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       transform: "translate(100px, 0px)" },
   ];
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
    'animations with CSS variables as keyframe values');
 
@@ -686,22 +687,22 @@ test(function(t) {
   var div = addDiv(t);
   div.style.animation = 'anim-variables-shorthand 100s';
 
   var frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   var expected = [
-    { offset: 0, computedOffset: 0, easing: "ease",
+    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
       marginBottom: "0px",
       marginLeft: "0px",
       marginRight: "0px",
       marginTop: "0px" },
-    { offset: 1, computedOffset: 1, easing: "ease",
+    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       marginBottom: "100px",
       marginLeft: "100px",
       marginRight: "100px",
       marginTop: "100px" },
   ];
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
@@ -712,19 +713,19 @@ test(function(t) {
   var div = addDiv(t);
   div.style.animation = 'anim-custom-property-in-keyframe 100s';
 
   var frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   var expected = [
-    { offset: 0, computedOffset: 0, easing: "ease",
+    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
       color: "rgb(0, 0, 0)" },
-    { offset: 1, computedOffset: 1, easing: "ease",
+    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       color: "rgb(0, 255, 0)" },
   ];
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
    'animations with a CSS variable which is overriden by the value in keyframe');
 
@@ -732,19 +733,19 @@ test(function(t) {
   var div = addDiv(t);
   div.style.animation = 'anim-only-custom-property-in-keyframe 100s';
 
   var frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   var expected = [
-    { offset: 0, computedOffset: 0, easing: "ease",
+    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
       transform: "translate(100px, 0px)" },
-    { offset: 1, computedOffset: 1, easing: "ease",
+    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
       transform: "none" },
   ];
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getKeyframes() returns expected values for ' +
    'animations with only custom property in a keyframe');
 
--- a/dom/animation/test/css-transitions/file_keyframeeffect-getkeyframes.html
+++ b/dom/animation/test/css-transitions/file_keyframeeffect-getkeyframes.html
@@ -31,18 +31,20 @@ test(function(t) {
   div.style.transition = 'left 100s';
   div.style.left = '100px';
 
   var frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   var expected = [
-    { offset: 0, computedOffset: 0, easing: "ease", left: "0px" },
-    { offset: 1, computedOffset: 1, easing: "linear", left: "100px" },
+    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
+      left: "0px" },
+    { offset: 1, computedOffset: 1, easing: "linear", composite: null,
+      left: "100px" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for a simple'
    + ' transition');
 
@@ -54,18 +56,20 @@ test(function(t) {
   div.style.transition = 'left 100s steps(2,end)';
   div.style.left = '100px';
 
   var frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   var expected = [
-    { offset: 0, computedOffset: 0, easing: "steps(2)", left: "0px" },
-    { offset: 1, computedOffset: 1, easing: "linear", left: "100px" },
+    { offset: 0, computedOffset: 0, easing: "steps(2)", composite: null,
+      left: "0px" },
+    { offset: 1, computedOffset: 1, easing: "linear", composite: null,
+      left: "100px" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for a simple'
    + ' transition with a non-default easing function');
 
@@ -76,18 +80,20 @@ test(function(t) {
   div.style.transition = 'left 100s';
   div.style.left = 'var(--var-100px)';
 
   var frames = getKeyframes(div);
 
   // CSS transition endpoints are based on the computed value so we
   // shouldn't see the variable reference
   var expected = [
-    { offset: 0, computedOffset: 0, easing: 'ease', left: '0px' },
-    { offset: 1, computedOffset: 1, easing: 'linear', left: '100px' },
+    { offset: 0, computedOffset: 0, easing: 'ease', composite: null,
+      left: '0px' },
+    { offset: 1, computedOffset: 1, easing: 'linear', composite: null,
+      left: '100px' },
   ];
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffectReadOnly.getKeyframes() returns expected frames for a'
    + ' transition with a CSS variable endpoint');
 
 done();
--- a/dom/webidl/BaseKeyframeTypes.webidl
+++ b/dom/webidl/BaseKeyframeTypes.webidl
@@ -17,23 +17,23 @@ enum CompositeOperation { "replace", "ad
 
 // The following dictionary types are not referred to by other .webidl files,
 // but we use it for manual JS->IDL and IDL->JS conversions in
 // KeyframeEffectReadOnly's implementation.
 
 dictionary BasePropertyIndexedKeyframe {
   (double? or sequence<double?>) offset = [];
   (DOMString or sequence<DOMString>) easing = [];
-  (CompositeOperation or sequence<CompositeOperation>) composite = [];
+  (CompositeOperation? or sequence<CompositeOperation?>) composite = [];
 };
 
 dictionary BaseKeyframe {
   double? offset = null;
   DOMString easing = "linear";
-  CompositeOperation composite;
+  CompositeOperation? composite = null;
 
   // Non-standard extensions
 
   // Member to allow testing when StyleAnimationValue::ComputeValues fails.
   //
   // Note that we currently only apply this to shorthand properties since
   // it's easier to annotate shorthand property values and because we have
   // only ever observed ComputeValues failing on shorthand values.
--- a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/composite.html
+++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/composite.html
@@ -24,18 +24,18 @@ test(t => {
                 'The effect composite value should be replaced');
 }, 'Change composite value');
 
 test(t => {
   const anim = createDiv(t).animate({ left: '10px' });
 
   anim.effect.composite = 'add';
   const keyframes = anim.effect.getKeyframes();
-  assert_equals(keyframes[0].composite, undefined,
-                'unspecified keyframe composite value should be absent even ' +
+  assert_equals(keyframes[0].composite, null,
+                'unspecified keyframe composite value should be null even ' +
                 'if effect composite is set');
 }, 'Unspecified keyframe composite value when setting effect composite');
 
 test(t => {
   const anim = createDiv(t).animate({ left: '10px', composite: 'replace' });
 
   anim.effect.composite = 'add';
   const keyframes = anim.effect.getKeyframes();
--- a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/constructor.html
+++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/constructor.html
@@ -52,17 +52,17 @@ test(t => {
 test(t => {
   const getKeyframe =
     composite => ({ left: [ '10px', '20px' ], composite: composite });
   for (const composite of gGoodKeyframeCompositeValueTests) {
     const effect = new KeyframeEffectReadOnly(target, getKeyframe(composite));
     assert_equals(effect.getKeyframes()[0].composite, composite,
                   `resulting composite for '${composite}'`);
   }
-  for (const composite of gBadCompositeValueTests) {
+  for (const composite of gBadKeyframeCompositeValueTests) {
     assert_throws(new TypeError, () => {
       new KeyframeEffectReadOnly(target, getKeyframe(composite));
     });
   }
 }, 'composite values are parsed correctly when passed to the ' +
    'KeyframeEffectReadOnly constructor in property-indexed keyframes');
 
 test(t => {
@@ -71,40 +71,40 @@ test(t => {
       { offset: 0, left: '10px', composite: composite },
       { offset: 1, left: '20px' }
     ];
   for (const composite of gGoodKeyframeCompositeValueTests) {
     const effect = new KeyframeEffectReadOnly(target, getKeyframes(composite));
     assert_equals(effect.getKeyframes()[0].composite, composite,
                   `resulting composite for '${composite}'`);
   }
-  for (const composite of gBadCompositeValueTests) {
+  for (const composite of gBadKeyframeCompositeValueTests) {
     assert_throws(new TypeError, () => {
       new KeyframeEffectReadOnly(target, getKeyframes(composite));
     });
   }
 }, 'composite values are parsed correctly when passed to the ' +
    'KeyframeEffectReadOnly constructor in regular keyframes');
 
 test(t => {
   for (const composite of gGoodOptionsCompositeValueTests) {
     const effect = new KeyframeEffectReadOnly(target, {
       left: ['10px', '20px']
     }, { composite: composite });
-    assert_equals(effect.getKeyframes()[0].composite, undefined,
+    assert_equals(effect.getKeyframes()[0].composite, null,
                   `resulting composite for '${composite}'`);
   }
-  for (const composite of gBadCompositeValueTests) {
+  for (const composite of gBadOptionsCompositeValueTests) {
     assert_throws(new TypeError, () => {
       new KeyframeEffectReadOnly(target, {
         left: ['10px', '20px']
       }, { composite: composite });
     });
   }
-}, 'composite value is absent if the composite operation specified on the ' +
+}, 'composite value is null if the composite operation specified on the ' +
    'keyframe effect is being used');
 
 for (const subtest of gKeyframesTests) {
   test(t => {
     const effect = new KeyframeEffectReadOnly(target, subtest.input);
     assert_frame_lists_equal(effect.getKeyframes(), subtest.output);
   }, `A KeyframeEffectReadOnly can be constructed with ${subtest.desc}`);
 
--- a/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/processing-a-keyframes-argument-001.html
+++ b/testing/web-platform/tests/web-animations/interfaces/KeyframeEffect/processing-a-keyframes-argument-001.html
@@ -175,67 +175,139 @@ function createIterable(iterations) {
 test(() => {
   const effect = new KeyframeEffect(null, createIterable([
     { done: false, value: { left: '100px' } },
     { done: false, value: { left: '300px' } },
     { done: false, value: { left: '200px' } },
     { done: true },
   ]));
   assert_frame_lists_equal(effect.getKeyframes(), [
-    { offset: null, computedOffset: 0, easing: 'linear', left: '100px' },
-    { offset: null, computedOffset: 0.5, easing: 'linear', left: '300px' },
-    { offset: null, computedOffset: 1, easing: 'linear', left: '200px' },
+    {
+      offset: null,
+      computedOffset: 0,
+      easing: 'linear',
+      left: '100px',
+      composite: null,
+    },
+    {
+      offset: null,
+      computedOffset: 0.5,
+      easing: 'linear',
+      left: '300px',
+      composite: null,
+    },
+    {
+      offset: null,
+      computedOffset: 1,
+      easing: 'linear',
+      left: '200px',
+      composite: null,
+    },
   ]);
 }, 'Keyframes are read from a custom iterator');
 
 test(() => {
   const keyframes = createIterable([
     { done: false, value: { left: '100px' } },
     { done: false, value: { left: '300px' } },
     { done: false, value: { left: '200px' } },
     { done: true },
   ]);
   keyframes.easing = 'ease-in-out';
   keyframes.offset = '0.1';
   const effect = new KeyframeEffect(null, keyframes);
   assert_frame_lists_equal(effect.getKeyframes(), [
-    { offset: null, computedOffset: 0, easing: 'linear', left: '100px' },
-    { offset: null, computedOffset: 0.5, easing: 'linear', left: '300px' },
-    { offset: null, computedOffset: 1, easing: 'linear', left: '200px' },
+    {
+      offset: null,
+      computedOffset: 0,
+      easing: 'linear',
+      left: '100px',
+      composite: null,
+    },
+    {
+      offset: null,
+      computedOffset: 0.5,
+      easing: 'linear',
+      left: '300px',
+      composite: null,
+    },
+    {
+      offset: null,
+      computedOffset: 1,
+      easing: 'linear',
+      left: '200px',
+      composite: null,
+    },
   ]);
 }, '\'easing\' and \'offset\' are ignored on iterable objects');
 
 test(() => {
   const effect = new KeyframeEffect(null, createIterable([
     { done: false, value: { left: '100px', top: '200px' } },
     { done: false, value: { left: '300px' } },
     { done: false, value: { left: '200px', top: '100px' } },
     { done: true },
   ]));
   assert_frame_lists_equal(effect.getKeyframes(), [
-    { offset: null, computedOffset: 0, easing: 'linear', left: '100px',
-      top: '200px' },
-    { offset: null, computedOffset: 0.5, easing: 'linear', left: '300px' },
-    { offset: null, computedOffset: 1, easing: 'linear', left: '200px',
-      top: '100px' },
+    {
+      offset: null,
+      computedOffset: 0,
+      easing: 'linear',
+      left: '100px',
+      top: '200px',
+      composite: null,
+    },
+    {
+      offset: null,
+      computedOffset: 0.5,
+      easing: 'linear',
+      left: '300px',
+      composite: null,
+    },
+    {
+      offset: null,
+      computedOffset: 1,
+      easing: 'linear',
+      left: '200px',
+      top: '100px',
+      composite: null,
+    },
   ]);
 }, 'Keyframes are read from a custom iterator with multiple properties'
    + ' specified');
 
 test(() => {
   const effect = new KeyframeEffect(null, createIterable([
     { done: false, value: { left: '100px' } },
     { done: false, value: { left: '250px', offset: 0.75 } },
     { done: false, value: { left: '200px' } },
     { done: true },
   ]));
   assert_frame_lists_equal(effect.getKeyframes(), [
-    { offset: null, computedOffset: 0, easing: 'linear', left: '100px' },
-    { offset: 0.75, computedOffset: 0.75, easing: 'linear', left: '250px' },
-    { offset: null, computedOffset: 1, easing: 'linear', left: '200px' },
+    {
+      offset: null,
+      computedOffset: 0,
+      easing: 'linear',
+      left: '100px',
+      composite: null,
+    },
+    {
+      offset: 0.75,
+      computedOffset: 0.75,
+      easing: 'linear',
+      left: '250px',
+      composite: null,
+    },
+    {
+      offset: null,
+      computedOffset: 1,
+      easing: 'linear',
+      left: '200px',
+      composite: null,
+    },
   ]);
 }, 'Keyframes are read from a custom iterator with where an offset is'
    + ' specified');
 
 test(() => {
   assert_throws({ name: 'TypeError' }, () => {
     new KeyframeEffect(null, createIterable([
       { done: false, value: { left: '100px' } },
@@ -248,35 +320,47 @@ test(() => {
    + ' should throw');
 
 test(() => {
   const effect = new KeyframeEffect(null, createIterable([
     { done: false, value: { left: ['100px', '200px'] } },
     { done: true },
   ]));
   assert_frame_lists_equal(effect.getKeyframes(), [
-    { offset: null, computedOffset: 1, easing: 'linear' }
+    { offset: null, computedOffset: 1, easing: 'linear', composite: null }
   ]);
 }, 'A list of values returned from a custom iterator should be ignored');
 
 test(() => {
   const keyframe = {};
   Object.defineProperty(keyframe, 'width', { value: '200px' });
   Object.defineProperty(keyframe, 'height', {
     value: '100px',
     enumerable: true,
   });
   assert_equals(keyframe.width, '200px', 'width of keyframe is readable');
   assert_equals(keyframe.height, '100px', 'height of keyframe is readable');
 
   const effect = new KeyframeEffect(null, [keyframe, { height: '200px' }]);
 
   assert_frame_lists_equal(effect.getKeyframes(), [
-    { offset: null, computedOffset: 0, easing: 'linear', height: '100px' },
-    { offset: null, computedOffset: 1, easing: 'linear', height: '200px' },
+    {
+      offset: null,
+      computedOffset: 0,
+      easing: 'linear',
+      height: '100px',
+      composite: null,
+    },
+    {
+      offset: null,
+      computedOffset: 1,
+      easing: 'linear',
+      height: '200px',
+      composite: null,
+    },
   ]);
 }, 'Only enumerable properties on keyframes are read');
 
 test(() => {
   const KeyframeParent = function() { this.width = '100px'; };
   KeyframeParent.prototype = { height: '100px' };
   const Keyframe = function() { this.top = '100px'; };
   Keyframe.prototype = Object.create(KeyframeParent.prototype);
@@ -284,34 +368,58 @@ test(() => {
     value: '100px',
     enumerable: true,
   });
   const keyframe = new Keyframe();
 
   const effect = new KeyframeEffect(null, [keyframe, { top: '200px' }]);
 
   assert_frame_lists_equal(effect.getKeyframes(), [
-    { offset: null, computedOffset: 0, easing: 'linear', top: '100px' },
-    { offset: null, computedOffset: 1, easing: 'linear', top: '200px' },
+    {
+      offset: null,
+      computedOffset: 0,
+      easing: 'linear',
+      top: '100px',
+      composite: null,
+    },
+    {
+      offset: null,
+      computedOffset: 1,
+      easing: 'linear',
+      top: '200px',
+      composite: null,
+    },
   ]);
 }, 'Only properties defined directly on keyframes are read');
 
 test(() => {
   const keyframes = {};
   Object.defineProperty(keyframes, 'width', ['100px', '200px']);
   Object.defineProperty(keyframes, 'height', {
     value: ['100px', '200px'],
     enumerable: true,
   });
 
   const effect = new KeyframeEffect(null, keyframes);
 
   assert_frame_lists_equal(effect.getKeyframes(), [
-    { offset: null, computedOffset: 0, easing: 'linear', height: '100px' },
-    { offset: null, computedOffset: 1, easing: 'linear', height: '200px' },
+    {
+      offset: null,
+      computedOffset: 0,
+      easing: 'linear',
+      height: '100px',
+      composite: null,
+    },
+    {
+      offset: null,
+      computedOffset: 1,
+      easing: 'linear',
+      height: '200px',
+      composite: null,
+    },
   ]);
 }, 'Only enumerable properties on property-indexed keyframes are read');
 
 test(() => {
   const KeyframesParent = function() { this.width = '100px'; };
   KeyframesParent.prototype = { height: '100px' };
   const Keyframes = function() { this.top = ['100px', '200px']; };
   Keyframes.prototype = Object.create(KeyframesParent.prototype);
@@ -319,18 +427,30 @@ test(() => {
     value: ['100px', '200px'],
     enumerable: true,
   });
   const keyframes = new Keyframes();
 
   const effect = new KeyframeEffect(null, keyframes);
 
   assert_frame_lists_equal(effect.getKeyframes(), [
-    { offset: null, computedOffset: 0, easing: 'linear', top: '100px' },
-    { offset: null, computedOffset: 1, easing: 'linear', top: '200px' },
+    {
+      offset: null,
+      computedOffset: 0,
+      easing: 'linear',
+      top: '100px',
+      composite: null,
+    },
+    {
+      offset: null,
+      computedOffset: 1,
+      easing: 'linear',
+      top: '200px',
+      composite: null,
+    },
   ]);
 }, 'Only properties defined directly on property-indexed keyframes are read');
 
 test(() => {
   const expectedOrder = ['composite', 'easing', 'offset', 'left', 'marginLeft'];
   const actualOrder = [];
   const kf1 = {};
   for (const {prop, value} of [{ prop: 'marginLeft', value: '10px' },
--- a/testing/web-platform/tests/web-animations/resources/keyframe-tests.js
+++ b/testing/web-platform/tests/web-animations/resources/keyframe-tests.js
@@ -7,24 +7,28 @@
 // ==============================
 
 
 // ------------------------------
 //  Composite values
 // ------------------------------
 
 const gGoodKeyframeCompositeValueTests = [
-  'replace', 'add', 'accumulate', undefined
+  'replace', 'add', 'accumulate', null
+];
+
+const gBadKeyframeCompositeValueTests = [
+  'unrecognised', 'replace ', 'Replace'
 ];
 
 const gGoodOptionsCompositeValueTests = [
   'replace', 'add', 'accumulate'
 ];
 
-const gBadCompositeValueTests = [
+const gBadOptionsCompositeValueTests = [
   'unrecognised', 'replace ', 'Replace', null
 ];
 
 // ------------------------------
 //  Keyframes
 // ------------------------------
 
 const gEmptyKeyframeListTests = [
@@ -45,19 +49,17 @@ const computedOffset = computedOffset =>
   computedOffset,
 });
 
 const keyframe = (offset, props, easing='linear', composite) => {
   // The object spread operator is not yet available in all browsers so we use
   // Object.assign instead.
   const result = {};
   Object.assign(result, offset, props, { easing });
-  if (composite) {
-    result.composite = composite;
-  }
+  result.composite = composite || null;
   return result;
 };
 
 const gKeyframesTests = [
 
   // ----------- Property-indexed keyframes: property handling -----------
 
   {