Bug 1483404 - Use "auto" values for unspecified keyframe-specific composite operations; r=smaug
authorBrian Birtles <birtles@gmail.com>
Wed, 15 Aug 2018 09:51:28 +0900
changeset 431839 a972e755df1b9809ea45aaab942dffea06105900
parent 431838 d795b45b87bcc54ea3bc1b3e86b94b58f6002e25
child 431840 cbfd7962458a790d91ae579c7e6f33ba046380f5
push id34451
push userebalazs@mozilla.com
push dateThu, 16 Aug 2018 09:25:15 +0000
treeherdermozilla-central@161817e6d127 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1483404
milestone63.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 1483404 - Use "auto" values for unspecified keyframe-specific composite operations; r=smaug Summary: This brings our implementation into line with the following spec change: https://github.com/w3c/csswg-drafts/commit/ced6b5aac02e0e021001f10e4d63e03456686dad See https://github.com/w3c/csswg-drafts/issues/3000 Reviewers: smaug Tags: #secure-revision Bug #: 1483404 Differential Revision: https://phabricator.services.mozilla.com/D3384
dom/animation/KeyframeEffect.cpp
dom/animation/KeyframeUtils.cpp
dom/animation/KeyframeUtils.h
dom/animation/test/css-transitions/test_keyframeeffect-getkeyframes.html
dom/webidl/BaseKeyframeTypes.webidl
testing/web-platform/meta/MANIFEST.json
testing/web-platform/tests/css/css-animations/KeyframeEffect-getKeyframes.tentative.html
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/KeyframeEffect.cpp
+++ b/dom/animation/KeyframeEffect.cpp
@@ -1094,19 +1094,18 @@ KeyframeEffect::GetKeyframes(JSContext*&
     MOZ_ASSERT(keyframe.mComputedOffset != Keyframe::kComputedOffsetNotSet,
                "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.SetValue(keyframe.mComposite.value());
-    }
+    keyframeDict.mComposite =
+      KeyframeUtils::ToCompositeOperationOrAuto(keyframe.mComposite);
 
     JS::Rooted<JS::Value> keyframeJSValue(aCx);
     if (!ToJSValue(aCx, keyframeDict, &keyframeJSValue)) {
       aRv.Throw(NS_ERROR_FAILURE);
       return;
     }
 
     RefPtr<RawServoDeclarationBlock> customProperties;
--- a/dom/animation/KeyframeUtils.cpp
+++ b/dom/animation/KeyframeUtils.cpp
@@ -446,19 +446,19 @@ ConvertKeyframeSequence(JSContext* aCx,
     Keyframe* keyframe = aResult.AppendElement(fallible);
     if (!keyframe) {
       return false;
     }
     if (!keyframeDict.mOffset.IsNull()) {
       keyframe->mOffset.emplace(keyframeDict.mOffset.Value());
     }
 
-    if (StaticPrefs::dom_animations_api_compositing_enabled() &&
-        !keyframeDict.mComposite.IsNull()) {
-      keyframe->mComposite.emplace(keyframeDict.mComposite.Value());
+    if (StaticPrefs::dom_animations_api_compositing_enabled()) {
+      keyframe->mComposite =
+        KeyframeUtils::ToCompositeOperation(keyframeDict.mComposite);
     }
 
     // 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,
                                   ListAllowance::eDisallow,
@@ -1183,37 +1183,35 @@ GetKeyframeListFromPropertyIndexedKeyfra
     }
   }
 
   // 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
   if (StaticPrefs::dom_animations_api_compositing_enabled()) {
-    const FallibleTArray<Nullable<dom::CompositeOperation>>* compositeOps =
-      nullptr;
-    AutoTArray<Nullable<dom::CompositeOperation>, 1> singleCompositeOp;
+    const FallibleTArray<dom::CompositeOperationOrAuto>* compositeOps = nullptr;
+    AutoTArray<dom::CompositeOperationOrAuto, 1> singleCompositeOp;
     auto& composite = keyframeDict.mComposite;
-    if (composite.IsCompositeOperation()) {
-      singleCompositeOp.AppendElement(composite.GetAsCompositeOperation());
-      const FallibleTArray<Nullable<dom::CompositeOperation>>& asFallibleArray =
+    if (composite.IsCompositeOperationOrAuto()) {
+      singleCompositeOp.AppendElement(
+        composite.GetAsCompositeOperationOrAuto());
+      const FallibleTArray<dom::CompositeOperationOrAuto>& asFallibleArray =
         singleCompositeOp;
       compositeOps = &asFallibleArray;
-    } else if (composite.IsCompositeOperationOrNullSequence()) {
-      compositeOps = &composite.GetAsCompositeOperationOrNullSequence();
+    } else if (composite.IsCompositeOperationOrAutoSequence()) {
+      compositeOps = &composite.GetAsCompositeOperationOrAutoSequence();
     }
 
     // Fill in and repeat as needed.
     if (compositeOps && !compositeOps->IsEmpty()) {
       size_t length = compositeOps->Length();
       for (size_t i = 0; i < aResult.Length(); i++) {
-        if (!compositeOps->ElementAt(i % length).IsNull()) {
-          aResult[i].mComposite.emplace(
-            compositeOps->ElementAt(i % length).Value());
-        }
+        dom::CompositeOperationOrAuto op = compositeOps->ElementAt(i % length);
+        aResult[i].mComposite = KeyframeUtils::ToCompositeOperation(op);
       }
     }
   }
 }
 
 /**
  * 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%
--- a/dom/animation/KeyframeUtils.h
+++ b/dom/animation/KeyframeUtils.h
@@ -99,13 +99,42 @@ public:
    * its subproperties, is animatable.
    *
    * @param aProperty The property to check.
    * @param aBackend  The style backend, Servo or Gecko, that should determine
    *                  if the property is animatable or not.
    * @return true if |aProperty| is animatable.
    */
   static bool IsAnimatableProperty(nsCSSPropertyID aProperty);
+
+  /*
+   * The spec defines two enums: CompositeOperation and
+   * CompositeOperationOrAuto.
+   *
+   * Internally, however, it's more convenient to always deal with
+   * CompositeOperation, represent the 'auto' case as a Nothing() value, and
+   * convert to and from CompositeOperationOrAuto at the API boundary.
+   *
+   * The following methods convert between these two representations and allow
+   * us to encapsulate the assumption that CompositeOperation is a strict subset
+   * of CompositeOperationOrAuto, in one location.
+   */
+
+  static dom::CompositeOperationOrAuto
+  ToCompositeOperationOrAuto(const Maybe<dom::CompositeOperation>& aComposite)
+  {
+    return aComposite
+           ? static_cast<dom::CompositeOperationOrAuto>(aComposite.value())
+           : dom::CompositeOperationOrAuto::Auto;
+  }
+
+  static Maybe<dom::CompositeOperation>
+  ToCompositeOperation(dom::CompositeOperationOrAuto aComposite)
+  {
+    return aComposite == dom::CompositeOperationOrAuto::Auto
+           ? Nothing()
+           : Some(static_cast<dom::CompositeOperation>(aComposite));
+  }
 };
 
 } // namespace mozilla
 
 #endif // mozilla_KeyframeUtils_h
--- a/dom/animation/test/css-transitions/test_keyframeeffect-getkeyframes.html
+++ b/dom/animation/test/css-transitions/test_keyframeeffect-getkeyframes.html
@@ -34,19 +34,19 @@ 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", composite: null,
+    { offset: 0, computedOffset: 0, easing: "ease", composite: "auto",
       left: "0px" },
-    { offset: 1, computedOffset: 1, easing: "linear", composite: null,
+    { offset: 1, computedOffset: 1, easing: "linear", composite: "auto",
       left: "100px" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffect.getKeyframes() returns expected frames for a simple'
    + ' transition');
@@ -59,19 +59,19 @@ 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)", composite: null,
+    { offset: 0, computedOffset: 0, easing: "steps(2)", composite: "auto",
       left: "0px" },
-    { offset: 1, computedOffset: 1, easing: "linear", composite: null,
+    { offset: 1, computedOffset: 1, easing: "linear", composite: "auto",
       left: "100px" },
   ];
 
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffect.getKeyframes() returns expected frames for a simple'
    + ' transition with a non-default easing function');
@@ -83,19 +83,19 @@ 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', composite: null,
+    { offset: 0, computedOffset: 0, easing: 'ease', composite: 'auto',
       left: '0px' },
-    { offset: 1, computedOffset: 1, easing: 'linear', composite: null,
+    { offset: 1, computedOffset: 1, easing: 'linear', composite: 'auto',
       left: '100px' },
   ];
   for (var i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffect.getKeyframes() returns expected frames for a'
    + ' transition with a CSS variable endpoint');
 
--- a/dom/webidl/BaseKeyframeTypes.webidl
+++ b/dom/webidl/BaseKeyframeTypes.webidl
@@ -10,30 +10,39 @@
  * https://drafts.csswg.org/web-animations/#dictdef-basecomputedkeyframe
  *
  * Copyright © 2016 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C
  * liability, trademark and document use rules apply.
  */
 
 enum CompositeOperation { "replace", "add", "accumulate" };
 
+// NOTE: The order of the values in this enum are important.
+//
+// We assume that CompositeOperation is a subset of CompositeOperationOrAuto so
+// that we can cast between the two types (provided the value is not "auto").
+//
+// If that assumption ceases to hold we will need to update the conversion
+// routines in KeyframeUtils.
+enum CompositeOperationOrAuto { "replace", "add", "accumulate", "auto" };
+
 // 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 KeyframeEffect's
 // implementation.
 
 dictionary BasePropertyIndexedKeyframe {
   (double? or sequence<double?>) offset = [];
   (DOMString or sequence<DOMString>) easing = [];
-  (CompositeOperation? or sequence<CompositeOperation?>) composite = [];
+  (CompositeOperationOrAuto or sequence<CompositeOperationOrAuto>) composite = [];
 };
 
 dictionary BaseKeyframe {
   double? offset = null;
   DOMString easing = "linear";
-  CompositeOperation? composite = null;
+  CompositeOperationOrAuto composite = "auto";
 
   // 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/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -514128,17 +514128,17 @@
    "ca6a23c13b0e2552fe60506e4ca9ad5e98b91330",
    "testharness"
   ],
   "css/css-animations/Element-getAnimations.tentative.html": [
    "ee80cacf15b8741c8cebbcaf46b113fcf20fa308",
    "testharness"
   ],
   "css/css-animations/KeyframeEffect-getKeyframes.tentative.html": [
-   "62473db7f3a0f2b4a21926c62d857a1ed74cbb1b",
+   "280f32a4852abaeb847092032fa49c9a6cb9797d",
    "testharness"
   ],
   "css/css-animations/KeyframeEffect-target.tentative.html": [
    "cc066b4ec19fb80c93e531faba754a07a98efe73",
    "testharness"
   ],
   "css/css-animations/META.yml": [
    "3ef19970007d4bb6a889f9601bc7c910f619f841",
@@ -644740,37 +644740,37 @@
    "e4714be02f8e31aa3663f372ab04c8a466226058",
    "testharness"
   ],
   "web-animations/interfaces/DocumentTimeline/idlharness.window.js": [
    "395d133f482a38a33ea5be061450f0f322be9fc9",
    "testharness"
   ],
   "web-animations/interfaces/KeyframeEffect/composite.html": [
-   "82ac633f0a9e817d6101299d995b0f13f5812f86",
+   "bcca2cad243abb3fe4d43dc2052e08af3e58499d",
    "testharness"
   ],
   "web-animations/interfaces/KeyframeEffect/constructor.html": [
-   "250a8c7a836f7bee818781cc15ccc42ed7ec54c2",
+   "8f16aa18711e596c1c2c604595146e36601a606f",
    "testharness"
   ],
   "web-animations/interfaces/KeyframeEffect/copy-constructor.html": [
    "e3bc0db00a7e84b1b0116b78c86905fc70d076f5",
    "testharness"
   ],
   "web-animations/interfaces/KeyframeEffect/idlharness.window.js": [
    "22548861ae335c80858808cbe19f996f24a7edc9",
    "testharness"
   ],
   "web-animations/interfaces/KeyframeEffect/iterationComposite.html": [
    "bbb8ee2a3261fcb98cda7a83056467bc0b20dac6",
    "testharness"
   ],
   "web-animations/interfaces/KeyframeEffect/processing-a-keyframes-argument-001.html": [
-   "d46cf6752080d7d7bb39d0b8d6fd7557f76e5528",
+   "5c9ec84e8db79f73ac1423258a84bf0d18d70a5f",
    "testharness"
   ],
   "web-animations/interfaces/KeyframeEffect/processing-a-keyframes-argument-002.html": [
    "8233dc07c5de8d17619489e0d759c75c4d2a059f",
    "testharness"
   ],
   "web-animations/interfaces/KeyframeEffect/setKeyframes.html": [
    "675a705891fa6c75c233179e59cc73cc516ff48c",
@@ -644784,17 +644784,17 @@
    "fc43385388285dbcbd9430a3a99c2614d958b903",
    "support"
   ],
   "web-animations/resources/effect-tests.js": [
    "8a18ab13c6c02ee70099cf23965771014d047c52",
    "support"
   ],
   "web-animations/resources/keyframe-tests.js": [
-   "f32f5ced8e7f58572f70d6bcd68d9a1c822459d9",
+   "76a0481de9190d4272ca9261676ecea0f8e25ca9",
    "support"
   ],
   "web-animations/resources/keyframe-utils.js": [
    "971e82ed81d6fef46a65f11ec66331a9f1954dfc",
    "support"
   ],
   "web-animations/resources/timing-tests.js": [
    "4b0f021f74e09f6cbde5e27fc7cb8240b7bd9788",
--- a/testing/web-platform/tests/css/css-animations/KeyframeEffect-getKeyframes.tentative.html
+++ b/testing/web-platform/tests/css/css-animations/KeyframeEffect-getKeyframes.tentative.html
@@ -224,19 +224,19 @@ test(t => {
 
   div.style.animation = 'anim-simple 100s';
   const frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   const expected = [
     { offset: 0, computedOffset: 0, easing: "ease",
-      color: "rgb(0, 0, 0)", composite: null },
+      color: "rgb(0, 0, 0)", composite: "auto" },
     { offset: 1, computedOffset: 1, easing: "ease",
-      color: "rgb(255, 255, 255)", composite: null },
+      color: "rgb(255, 255, 255)", composite: "auto" },
   ];
 
   for (let i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffect.getKeyframes() returns expected frames for a simple'
    + ' animation');
 
@@ -294,20 +294,20 @@ test(t => {
   const div = addDiv(t);
 
   div.style.animation = 'anim-simple-shorthand 100s';
   const frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   const expected = [
-    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
+    { offset: 0, computedOffset: 0, easing: "ease", composite: "auto",
       marginBottom: "8px", marginLeft: "8px",
       marginRight: "8px", marginTop: "8px" },
-    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
+    { offset: 1, computedOffset: 1, easing: "ease", composite: "auto",
       marginBottom: "16px", marginLeft: "16px",
       marginRight: "16px", marginTop: "16px" },
   ];
 
   for (let i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffect.getKeyframes() returns expected frames for a simple'
@@ -318,19 +318,19 @@ test(t => {
 
   div.style.animation = 'anim-omit-to 100s';
   div.style.color = 'rgb(255, 255, 255)';
   const frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   const expected = [
-    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
+    { offset: 0, computedOffset: 0, easing: "ease", composite: "auto",
       color: "rgb(0, 0, 255)" },
-    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
+    { offset: 1, computedOffset: 1, easing: "ease", composite: "auto",
       color: "rgb(255, 255, 255)" },
   ];
 
   for (let i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffect.getKeyframes() returns expected frames for an ' +
    'animation with a 0% keyframe and no 100% keyframe');
@@ -340,19 +340,19 @@ test(t => {
 
   div.style.animation = 'anim-omit-from 100s';
   div.style.color = 'rgb(255, 255, 255)';
   const frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   const expected = [
-    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
+    { offset: 0, computedOffset: 0, easing: "ease", composite: "auto",
       color: "rgb(255, 255, 255)" },
-    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
+    { offset: 1, computedOffset: 1, easing: "ease", composite: "auto",
       color: "rgb(0, 0, 255)" },
   ];
 
   for (let i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffect.getKeyframes() returns expected frames for an ' +
    'animation with a 100% keyframe and no 0% keyframe');
@@ -362,21 +362,21 @@ test(t => {
 
   div.style.animation = 'anim-omit-from-to 100s';
   div.style.color = 'rgb(255, 255, 255)';
   const frames = getKeyframes(div);
 
   assert_equals(frames.length, 3, "number of frames");
 
   const expected = [
-    { offset: 0,   computedOffset: 0,   easing: "ease", composite: null,
+    { offset: 0,   computedOffset: 0,   easing: "ease", composite: "auto",
       color: "rgb(255, 255, 255)" },
-    { offset: 0.5, computedOffset: 0.5, easing: "ease", composite: null,
+    { offset: 0.5, computedOffset: 0.5, easing: "ease", composite: "auto",
       color: "rgb(0, 0, 255)" },
-    { offset: 1,   computedOffset: 1,   easing: "ease", composite: null,
+    { offset: 1,   computedOffset: 1,   easing: "ease", composite: "auto",
       color: "rgb(255, 255, 255)" },
   ];
 
   for (let i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffect.getKeyframes() returns expected frames for an ' +
    'animation with no 0% or 100% keyframe but with a 50% keyframe');
@@ -386,19 +386,19 @@ test(t => {
 
   div.style.animation = 'anim-partially-omit-to 100s';
   div.style.marginTop = '250px';
   const frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   const expected = [
-    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
+    { offset: 0, computedOffset: 0, easing: "ease", composite: "auto",
       marginTop: '50px', marginBottom: '100px' },
-    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
+    { offset: 1, computedOffset: 1, easing: "ease", composite: "auto",
       marginTop: '250px', marginBottom: '200px' },
   ];
 
   for (let i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffect.getKeyframes() returns expected frames for an ' +
    'animation with a partially complete 100% keyframe (because the ' +
@@ -408,23 +408,23 @@ test(t => {
   const div = addDiv(t);
 
   div.style.animation = 'anim-different-props 100s';
   const frames = getKeyframes(div);
 
   assert_equals(frames.length, 4, "number of frames");
 
   const expected = [
-    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
+    { offset: 0, computedOffset: 0, easing: "ease", composite: "auto",
       color: "rgb(0, 0, 0)", marginTop: "8px" },
-    { offset: 0.25, computedOffset: 0.25, easing: "ease", composite: null,
+    { offset: 0.25, computedOffset: 0.25, easing: "ease", composite: "auto",
       color: "rgb(0, 0, 255)" },
-    { offset: 0.75, computedOffset: 0.75, easing: "ease", composite: null,
+    { offset: 0.75, computedOffset: 0.75, easing: "ease", composite: "auto",
       marginTop: "12px" },
-    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
+    { offset: 1, computedOffset: 1, easing: "ease", composite: "auto",
       color: "rgb(255, 255, 255)", marginTop: "16px" },
   ];
 
   for (let i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffect.getKeyframes() returns expected frames for an ' +
    'animation with different properties on different keyframes, all ' +
@@ -434,23 +434,23 @@ test(t => {
   const div = addDiv(t);
 
   div.style.animation = 'anim-different-props-and-easing 100s';
   const frames = getKeyframes(div);
 
   assert_equals(frames.length, 4, "number of frames");
 
   const expected = [
-    { offset: 0, computedOffset: 0, easing: "linear", composite: null,
+    { offset: 0, computedOffset: 0, easing: "linear", composite: "auto",
       color: "rgb(0, 0, 0)", marginTop: "8px" },
-    { offset: 0.25, computedOffset: 0.25, easing: "steps(1)", composite: null,
+    { offset: 0.25, computedOffset: 0.25, easing: "steps(1)", composite: "auto",
       color: "rgb(0, 0, 255)" },
-    { offset: 0.75, computedOffset: 0.75, easing: "ease-in", composite: null,
+    { offset: 0.75, computedOffset: 0.75, easing: "ease-in", composite: "auto",
       marginTop: "12px" },
-    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
+    { offset: 1, computedOffset: 1, easing: "ease", composite: "auto",
       color: "rgb(255, 255, 255)", marginTop: "16px" },
   ];
 
   for (let i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffect.getKeyframes() returns expected frames for an ' +
    'animation with different properties on different keyframes, with ' +
@@ -460,19 +460,19 @@ test(t => {
   const div = addDiv(t);
 
   div.style.animation = 'anim-merge-offset 100s';
   const frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   const expected = [
-    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
+    { offset: 0, computedOffset: 0, easing: "ease", composite: "auto",
       color: "rgb(0, 0, 0)", marginTop: "8px" },
-    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
+    { offset: 1, computedOffset: 1, easing: "ease", composite: "auto",
       color: "rgb(255, 255, 255)", marginTop: "16px" },
   ];
 
   for (let i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffect.getKeyframes() returns expected frames for an ' +
    'animation with multiple keyframes for the same time, and all with ' +
@@ -482,21 +482,21 @@ test(t => {
   const div = addDiv(t);
 
   div.style.animation = 'anim-merge-offset-and-easing 100s';
   const frames = getKeyframes(div);
 
   assert_equals(frames.length, 3, "number of frames");
 
   const expected = [
-    { offset: 0, computedOffset: 0, easing: "steps(1)", composite: null,
+    { offset: 0, computedOffset: 0, easing: "steps(1)", composite: "auto",
       color: "rgb(0, 0, 0)", fontSize: "16px" },
-    { offset: 0, computedOffset: 0, easing: "linear", composite: null,
+    { offset: 0, computedOffset: 0, easing: "linear", composite: "auto",
       marginTop: "8px", paddingLeft: "2px" },
-    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
+    { offset: 1, computedOffset: 1, easing: "ease", composite: "auto",
       color: "rgb(255, 255, 255)", fontSize: "32px", marginTop: "16px",
       paddingLeft: "4px" },
   ];
 
   for (let i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffect.getKeyframes() returns expected frames for an ' +
@@ -507,21 +507,21 @@ test(t => {
   const div = addDiv(t);
 
   div.style.animation = 'anim-no-merge-equiv-easing 100s';
   const frames = getKeyframes(div);
 
   assert_equals(frames.length, 3, "number of frames");
 
   const expected = [
-    { offset: 0, computedOffset: 0, easing: "steps(1)", composite: null,
+    { offset: 0, computedOffset: 0, easing: "steps(1)", composite: "auto",
       marginTop: "0px", marginRight: "0px", marginBottom: "0px" },
-    { offset: 0.5, computedOffset: 0.5, easing: "steps(1)", composite: null,
+    { offset: 0.5, computedOffset: 0.5, easing: "steps(1)", composite: "auto",
       marginTop: "10px", marginRight: "10px", marginBottom: "10px" },
-    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
+    { offset: 1, computedOffset: 1, easing: "ease", composite: "auto",
       marginTop: "20px", marginRight: "20px", marginBottom: "20px" },
   ];
 
   for (let i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffect.getKeyframes() returns expected frames for an ' +
    'animation with multiple keyframes for the same time and with ' +
@@ -531,27 +531,27 @@ test(t => {
   const div = addDiv(t);
 
   div.style.animation = 'anim-overriding 100s';
   const frames = getKeyframes(div);
 
   assert_equals(frames.length, 6, "number of frames");
 
   const expected = [
-    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
+    { offset: 0, computedOffset: 0, easing: "ease", composite: "auto",
       paddingTop: "30px" },
-    { offset: 0.5, computedOffset: 0.5, easing: "ease", composite: null,
+    { offset: 0.5, computedOffset: 0.5, easing: "ease", composite: "auto",
       paddingTop: "20px" },
-    { offset: 0.75, computedOffset: 0.75, easing: "ease", composite: null,
+    { offset: 0.75, computedOffset: 0.75, easing: "ease", composite: "auto",
       paddingTop: "20px" },
-    { offset: 0.85, computedOffset: 0.85, easing: "ease", composite: null,
+    { offset: 0.85, computedOffset: 0.85, easing: "ease", composite: "auto",
       paddingTop: "30px" },
-    { offset: 0.851, computedOffset: 0.851, easing: "ease", composite: null,
+    { offset: 0.851, computedOffset: 0.851, easing: "ease", composite: "auto",
       paddingTop: "60px" },
-    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
+    { offset: 1, computedOffset: 1, easing: "ease", composite: "auto",
       paddingTop: "70px" },
   ];
 
   for (let i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffect.getKeyframes() returns expected frames for ' +
    'overlapping keyframes');
@@ -563,19 +563,19 @@ test(t => {
   const div = addDiv(t);
 
   div.style.animation = 'anim-filter 100s';
   const frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   const expected = [
-    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
+    { offset: 0, computedOffset: 0, easing: "ease", composite: "auto",
       filter: "none" },
-    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
+    { offset: 1, computedOffset: 1, easing: "ease", composite: "auto",
       filter: "blur(5px) sepia(60%) saturate(30%)" },
   ];
 
   for (let i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffect.getKeyframes() returns expected values for ' +
    'animations with filter properties and missing keyframes');
@@ -584,19 +584,19 @@ test(t => {
   const div = addDiv(t);
 
   div.style.animation = 'anim-filter-drop-shadow 100s';
   const frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   const expected = [
-    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
+    { offset: 0, computedOffset: 0, easing: "ease", composite: "auto",
       filter: "drop-shadow(rgb(0, 255, 0) 10px 10px 10px)" },
-    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
+    { offset: 1, computedOffset: 1, easing: "ease", composite: "auto",
       filter: "drop-shadow(rgb(255, 0, 0) 50px 30px 10px)" },
   ];
 
   for (let i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffect.getKeyframes() returns expected values for ' +
    'animation with drop-shadow of filter property');
@@ -612,21 +612,21 @@ test(t => {
                          '0 0 16px rgb(0, 0, 255), ' +
                          '0 0 3.2px rgb(0, 0, 255)';
   div.style.animation = 'anim-text-shadow 100s';
   const frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   const expected = [
-    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
+    { offset: 0, computedOffset: 0, easing: "ease", composite: "auto",
       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", composite: null,
+    { offset: 1, computedOffset: 1, easing: "ease", composite: "auto",
       textShadow: "none" },
   ];
 
   for (let i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffect.getKeyframes() returns expected values for ' +
    'animations with text-shadow properties and missing keyframes');
@@ -639,19 +639,19 @@ test(t => {
   const div = addDiv(t);
 
   div.style.animation = 'anim-background-size 100s';
   let frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   const expected = [
-    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
+    { offset: 0, computedOffset: 0, easing: "ease", composite: "auto",
       backgroundSize: "auto auto" },
-    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
+    { offset: 1, computedOffset: 1, easing: "ease", composite: "auto",
       backgroundSize: "50% auto, 6px auto, contain" },
   ];
 
   for (let i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 
   // Test inheriting a background-size value
@@ -671,19 +671,19 @@ test(t => {
   const div = addDiv(t);
   div.style.animation = 'anim-variables 100s';
 
   const frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   const expected = [
-    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
+    { offset: 0, computedOffset: 0, easing: "ease", composite: "auto",
       transform: "none" },
-    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
+    { offset: 1, computedOffset: 1, easing: "ease", composite: "auto",
       transform: "translate(100px, 0px)" },
   ];
   for (let i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffect.getKeyframes() returns expected values for ' +
    'animations with CSS variables as keyframe values');
 
@@ -691,22 +691,22 @@ test(t => {
   const div = addDiv(t);
   div.style.animation = 'anim-variables-shorthand 100s';
 
   const frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   const expected = [
-    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
+    { offset: 0, computedOffset: 0, easing: "ease", composite: "auto",
       marginBottom: "0px",
       marginLeft: "0px",
       marginRight: "0px",
       marginTop: "0px" },
-    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
+    { offset: 1, computedOffset: 1, easing: "ease", composite: "auto",
       marginBottom: "100px",
       marginLeft: "100px",
       marginRight: "100px",
       marginTop: "100px" },
   ];
   for (let i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
@@ -717,19 +717,19 @@ test(t => {
   const div = addDiv(t);
   div.style.animation = 'anim-custom-property-in-keyframe 100s';
 
   const frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   const expected = [
-    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
+    { offset: 0, computedOffset: 0, easing: "ease", composite: "auto",
       color: "rgb(0, 0, 0)" },
-    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
+    { offset: 1, computedOffset: 1, easing: "ease", composite: "auto",
       color: "rgb(0, 255, 0)" },
   ];
   for (let i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffect.getKeyframes() returns expected values for ' +
    'animations with a CSS variable which is overriden by the value in keyframe');
 
@@ -737,19 +737,19 @@ test(t => {
   const div = addDiv(t);
   div.style.animation = 'anim-only-custom-property-in-keyframe 100s';
 
   const frames = getKeyframes(div);
 
   assert_equals(frames.length, 2, "number of frames");
 
   const expected = [
-    { offset: 0, computedOffset: 0, easing: "ease", composite: null,
+    { offset: 0, computedOffset: 0, easing: "ease", composite: "auto",
       transform: "translate(100px, 0px)" },
-    { offset: 1, computedOffset: 1, easing: "ease", composite: null,
+    { offset: 1, computedOffset: 1, easing: "ease", composite: "auto",
       transform: "none" },
   ];
   for (let i = 0; i < frames.length; i++) {
     assert_frames_equal(frames[i], expected[i], "ComputedKeyframe #" + i);
   }
 }, 'KeyframeEffect.getKeyframes() returns expected values for ' +
    'animations with only custom property in a keyframe');
 
--- 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, null,
-                'unspecified keyframe composite value should be null even ' +
+  assert_equals(keyframes[0].composite, 'auto',
+                'unspecified keyframe composite value should be auto 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
@@ -83,27 +83,27 @@ test(t => {
 }, 'composite values are parsed correctly when passed to the ' +
    'KeyframeEffect constructor in regular keyframes');
 
 test(t => {
   for (const composite of gGoodOptionsCompositeValueTests) {
     const effect = new KeyframeEffect(target, {
       left: ['10px', '20px']
     }, { composite });
-    assert_equals(effect.getKeyframes()[0].composite, null,
+    assert_equals(effect.getKeyframes()[0].composite, 'auto',
                   `resulting composite for '${composite}'`);
   }
   for (const composite of gBadOptionsCompositeValueTests) {
     assert_throws(new TypeError, () => {
       new KeyframeEffect(target, {
         left: ['10px', '20px']
       }, { composite: composite });
     });
   }
-}, 'composite value is null if the composite operation specified on the ' +
+}, 'composite value is auto if the composite operation specified on the ' +
    'keyframe effect is being used');
 
 for (const subtest of gKeyframesTests) {
   test(t => {
     const effect = new KeyframeEffect(target, subtest.input);
     assert_frame_lists_equal(effect.getKeyframes(), subtest.output);
   }, `A KeyframeEffect 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
@@ -188,31 +188,31 @@ test(() => {
     { done: true },
   ]));
   assert_frame_lists_equal(effect.getKeyframes(), [
     {
       offset: null,
       computedOffset: 0,
       easing: 'linear',
       left: '100px',
-      composite: null,
+      composite: 'auto',
     },
     {
       offset: null,
       computedOffset: 0.5,
       easing: 'linear',
       left: '300px',
-      composite: null,
+      composite: 'auto',
     },
     {
       offset: null,
       computedOffset: 1,
       easing: 'linear',
       left: '200px',
-      composite: null,
+      composite: 'auto',
     },
   ]);
 }, 'Keyframes are read from a custom iterator');
 
 test(() => {
   const keyframes = createIterable([
     { done: false, value: { left: '100px' } },
     { done: false, value: { left: '300px' } },
@@ -223,31 +223,31 @@ test(() => {
   keyframes.offset = '0.1';
   const effect = new KeyframeEffect(null, keyframes);
   assert_frame_lists_equal(effect.getKeyframes(), [
     {
       offset: null,
       computedOffset: 0,
       easing: 'linear',
       left: '100px',
-      composite: null,
+      composite: 'auto',
     },
     {
       offset: null,
       computedOffset: 0.5,
       easing: 'linear',
       left: '300px',
-      composite: null,
+      composite: 'auto',
     },
     {
       offset: null,
       computedOffset: 1,
       easing: 'linear',
       left: '200px',
-      composite: null,
+      composite: 'auto',
     },
   ]);
 }, '\'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' } },
@@ -256,32 +256,32 @@ test(() => {
   ]));
   assert_frame_lists_equal(effect.getKeyframes(), [
     {
       offset: null,
       computedOffset: 0,
       easing: 'linear',
       left: '100px',
       top: '200px',
-      composite: null,
+      composite: 'auto',
     },
     {
       offset: null,
       computedOffset: 0.5,
       easing: 'linear',
       left: '300px',
-      composite: null,
+      composite: 'auto',
     },
     {
       offset: null,
       computedOffset: 1,
       easing: 'linear',
       left: '200px',
       top: '100px',
-      composite: null,
+      composite: 'auto',
     },
   ]);
 }, 'Keyframes are read from a custom iterator with multiple properties'
    + ' specified');
 
 test(() => {
   const effect = new KeyframeEffect(null, createIterable([
     { done: false, value: { left: '100px' } },
@@ -290,31 +290,31 @@ test(() => {
     { done: true },
   ]));
   assert_frame_lists_equal(effect.getKeyframes(), [
     {
       offset: null,
       computedOffset: 0,
       easing: 'linear',
       left: '100px',
-      composite: null,
+      composite: 'auto',
     },
     {
       offset: 0.75,
       computedOffset: 0.75,
       easing: 'linear',
       left: '250px',
-      composite: null,
+      composite: 'auto',
     },
     {
       offset: null,
       computedOffset: 1,
       easing: 'linear',
       left: '200px',
-      composite: null,
+      composite: 'auto',
     },
   ]);
 }, 'Keyframes are read from a custom iterator with where an offset is'
    + ' specified');
 
 test(() => {
   const test_error = { name: 'test' };
   const bad_keyframe = { get left() { throw test_error; } };
@@ -344,45 +344,45 @@ test(() => {
 test(() => {
   const effect = new KeyframeEffect(null, createIterable([
     { done: false, value: { left: '100px' } },
     { done: false },  // No value member; keyframe is undefined.
     { done: false, value: { left: '200px' } },
     { done: true },
   ]));
   assert_frame_lists_equal(effect.getKeyframes(), [
-    { left: '100px', offset: null, computedOffset: 0, easing: 'linear', composite: null },
-    { offset: null, computedOffset: 0.5, easing: 'linear', composite: null },
-    { left: '200px', offset: null, computedOffset: 1, easing: 'linear', composite: null },
+    { left: '100px', offset: null, computedOffset: 0, easing: 'linear', composite: 'auto' },
+    { offset: null, computedOffset: 0.5, easing: 'linear', composite: 'auto' },
+    { left: '200px', offset: null, computedOffset: 1, easing: 'linear', composite: 'auto' },
   ]);
 }, 'An undefined keyframe returned from a custom iterator should be treated as a'
     + ' default keyframe');
 
 test(() => {
   const effect = new KeyframeEffect(null, createIterable([
     { done: false, value: { left: '100px' } },
     { done: false, value: null },
     { done: false, value: { left: '200px' } },
     { done: true },
   ]));
   assert_frame_lists_equal(effect.getKeyframes(), [
-    { left: '100px', offset: null, computedOffset: 0, easing: 'linear', composite: null },
-    { offset: null, computedOffset: 0.5, easing: 'linear', composite: null },
-    { left: '200px', offset: null, computedOffset: 1, easing: 'linear', composite: null },
+    { left: '100px', offset: null, computedOffset: 0, easing: 'linear', composite: 'auto' },
+    { offset: null, computedOffset: 0.5, easing: 'linear', composite: 'auto' },
+    { left: '200px', offset: null, computedOffset: 1, easing: 'linear', composite: 'auto' },
   ]);
 }, 'A null keyframe returned from a custom iterator should be treated as a'
     + ' default keyframe');
 
 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', composite: null }
+    { offset: null, computedOffset: 1, easing: 'linear', composite: 'auto' }
   ]);
 }, 'A list of values returned from a custom iterator should be ignored');
 
 test(() => {
   const test_error = { name: 'test' };
   const keyframe_obj = {
     [Symbol.iterator]() {
       return { next() { throw test_error; } };
@@ -432,24 +432,24 @@ test(() => {
   const effect = new KeyframeEffect(null, [keyframe, { height: '200px' }]);
 
   assert_frame_lists_equal(effect.getKeyframes(), [
     {
       offset: null,
       computedOffset: 0,
       easing: 'linear',
       height: '100px',
-      composite: null,
+      composite: 'auto',
     },
     {
       offset: null,
       computedOffset: 1,
       easing: 'linear',
       height: '200px',
-      composite: null,
+      composite: 'auto',
     },
   ]);
 }, 'Only enumerable properties on keyframes are read');
 
 test(() => {
   const KeyframeParent = function() { this.width = '100px'; };
   KeyframeParent.prototype = { height: '100px' };
   const Keyframe = function() { this.top = '100px'; };
@@ -463,24 +463,24 @@ test(() => {
   const effect = new KeyframeEffect(null, [keyframe, { top: '200px' }]);
 
   assert_frame_lists_equal(effect.getKeyframes(), [
     {
       offset: null,
       computedOffset: 0,
       easing: 'linear',
       top: '100px',
-      composite: null,
+      composite: 'auto',
     },
     {
       offset: null,
       computedOffset: 1,
       easing: 'linear',
       top: '200px',
-      composite: null,
+      composite: 'auto',
     },
   ]);
 }, 'Only properties defined directly on keyframes are read');
 
 test(() => {
   const keyframes = {};
   Object.defineProperty(keyframes, 'width', ['100px', '200px']);
   Object.defineProperty(keyframes, 'height', {
@@ -491,24 +491,24 @@ test(() => {
   const effect = new KeyframeEffect(null, keyframes);
 
   assert_frame_lists_equal(effect.getKeyframes(), [
     {
       offset: null,
       computedOffset: 0,
       easing: 'linear',
       height: '100px',
-      composite: null,
+      composite: 'auto',
     },
     {
       offset: null,
       computedOffset: 1,
       easing: 'linear',
       height: '200px',
-      composite: null,
+      composite: 'auto',
     },
   ]);
 }, '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']; };
@@ -522,24 +522,24 @@ test(() => {
   const effect = new KeyframeEffect(null, keyframes);
 
   assert_frame_lists_equal(effect.getKeyframes(), [
     {
       offset: null,
       computedOffset: 0,
       easing: 'linear',
       top: '100px',
-      composite: null,
+      composite: 'auto',
     },
     {
       offset: null,
       computedOffset: 1,
       easing: 'linear',
       top: '200px',
-      composite: null,
+      composite: 'auto',
     },
   ]);
 }, 'Only properties defined directly on property-indexed keyframes are read');
 
 test(() => {
   const expectedOrder = ['composite', 'easing', 'offset', 'left', 'marginLeft'];
   const actualOrder = [];
   const kf1 = {};
--- a/testing/web-platform/tests/web-animations/resources/keyframe-tests.js
+++ b/testing/web-platform/tests/web-animations/resources/keyframe-tests.js
@@ -7,21 +7,21 @@
 // ==============================
 
 
 // ------------------------------
 //  Composite values
 // ------------------------------
 
 const gGoodKeyframeCompositeValueTests = [
-  'replace', 'add', 'accumulate', null
+  'replace', 'add', 'accumulate', 'auto'
 ];
 
 const gBadKeyframeCompositeValueTests = [
-  'unrecognised', 'replace ', 'Replace'
+  'unrecognised', 'replace ', 'Replace', null
 ];
 
 const gGoodOptionsCompositeValueTests = [
   'replace', 'add', 'accumulate'
 ];
 
 const gBadOptionsCompositeValueTests = [
   'unrecognised', 'replace ', 'Replace', null
@@ -49,17 +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 });
-  result.composite = composite || null;
+  result.composite = composite || 'auto';
   return result;
 };
 
 const gKeyframesTests = [
 
   // ----------- Property-indexed keyframes: property handling -----------
 
   {