<!doctype html>
<head>
<meta charset=utf-8>
<title>Bug 1196114 - Test metadata related to which animation properties
       are running on the compositor</title>
<script type="application/javascript" src="../testharness.js"></script>
<script type="application/javascript" src="../testharnessreport.js"></script>
<script type="application/javascript" src="../testcommon.js"></script>
<style>
.compositable {
  /* Element needs geometry to be eligible for layerization */
  width: 100px;
  height: 100px;
  background-color: white;
}
@keyframes fade {
  from { opacity: 1 }
  to   { opacity: 0 }
}
@keyframes translate {
  from { transform: none }
  to   { transform: translate(100px) }
}
</style>
</head>
<body>
<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1196114"
  target="_blank">Mozilla Bug 1196114</a>
<div id="log"></div>
<script>
'use strict';

// This is used for obtaining localized strings.
var gStringBundle;

W3CTest.runner.requestLongerTimeout(2);

const Services = SpecialPowers.Services;
Services.locale.requestedLocales = ["en-US"];

SpecialPowers.pushPrefEnv({ "set": [
                            // Need to set devPixelsPerPx explicitly to gain
                            // consistent pixel values in warning messages
                            // regardless of platform DPIs.
                            ["layout.css.devPixelsPerPx", 1],
                            ["layout.animation.prerender.partial", false],
                          ] },
                          start);

function compare_property_state(a, b) {
  if (a.property > b.property) {
    return -1;
  } else if (a.property < b.property) {
    return 1;
  }
  if (a.runningOnCompositor != b.runningOnCompositor) {
    return a.runningOnCompositor ? 1 : -1;
  }
  return a.warning > b.warning ? -1 : 1;
}

function assert_animation_property_state_equals(actual, expected) {
  assert_equals(actual.length, expected.length, 'Number of properties');

  var sortedActual = actual.sort(compare_property_state);
  var sortedExpected = expected.sort(compare_property_state);

  for (var i = 0; i < sortedActual.length; i++) {
    assert_equals(sortedActual[i].property,
                  sortedExpected[i].property,
                  'CSS property name should match');
    assert_equals(sortedActual[i].runningOnCompositor,
                  sortedExpected[i].runningOnCompositor,
                  'runningOnCompositor property should match');
    if (sortedExpected[i].warning instanceof RegExp) {
      assert_regexp_match(sortedActual[i].warning,
                          sortedExpected[i].warning,
                          'warning message should match');
    } else if (sortedExpected[i].warning) {
      assert_equals(sortedActual[i].warning,
                    gStringBundle.GetStringFromName(sortedExpected[i].warning),
                    'warning message should match');
    }
  }
}

// Check that the animation is running on compositor and
// warning property is not set for the CSS property regardless
// expected values.
function assert_all_properties_running_on_compositor(actual, expected) {
  assert_equals(actual.length, expected.length);

  var sortedActual = actual.sort(compare_property_state);
  var sortedExpected = expected.sort(compare_property_state);

  for (var i = 0; i < sortedActual.length; i++) {
    assert_equals(sortedActual[i].property,
                  sortedExpected[i].property,
                  'CSS property name should match');
    assert_true(sortedActual[i].runningOnCompositor,
                'runningOnCompositor property should be true on ' +
                sortedActual[i].property);
    assert_not_exists(sortedActual[i], 'warning',
                      'warning property should not be set');
  }
}

function testBasicOperation() {
  [
    {
      desc: 'animations on compositor',
      frames: {
        opacity: [0, 1]
      },
      expected: [
        {
          property: 'opacity',
          runningOnCompositor: true
        }
      ]
    },
    {
      desc: 'animations on main thread',
      frames: {
        zIndex: ['0', '999']
      },
      expected: [
        {
          property: 'z-index',
          runningOnCompositor: false
        }
      ]
    },
    {
      desc: 'animations on both threads',
      frames: {
        zIndex: ['0', '999'],
        transform: ['translate(0px)', 'translate(100px)']
      },
      expected: [
        {
          property: 'z-index',
          runningOnCompositor: false
        },
        {
          property: 'transform',
          runningOnCompositor: true
        }
      ]
    },
    {
      desc: 'two animation properties on compositor thread',
      frames: {
        opacity: [0, 1],
        transform: ['translate(0px)', 'translate(100px)']
      },
      expected: [
        {
          property: 'opacity',
          runningOnCompositor: true
        },
        {
          property: 'transform',
          runningOnCompositor: true
        }
      ]
    },
    {
      desc: 'two transform-like animation properties on compositor thread',
      frames: {
        transform: ['translate(0px)', 'translate(100px)'],
        translate: ['0px', '100px']
      },
      expected: [
        {
          property: 'transform',
          runningOnCompositor: true
        },
        {
          property: 'translate',
          runningOnCompositor: true
        }
      ]
    },
    {
      desc: 'opacity on compositor with animation of geometric properties',
      frames: {
        width: ['100px', '200px'],
        opacity: [0, 1]
      },
      expected: [
        {
          property: 'width',
          runningOnCompositor: false
        },
        {
          property: 'opacity',
          runningOnCompositor: true
        }
      ]
    },
  ].forEach(subtest => {
    promise_test(async t => {
      var animation = addDivAndAnimate(t, { class: 'compositable' },
                                       subtest.frames, 100 * MS_PER_SEC);
      await waitForPaints();
      assert_animation_property_state_equals(
        animation.effect.getProperties(),
        subtest.expected);
    }, subtest.desc);
  });
}

// Test adding/removing a 'width' property on the same animation object.
function testKeyframesWithGeometricProperties() {
  [
    {
      desc: 'transform',
      frames: {
        transform: ['translate(0px)', 'translate(100px)']
      },
      expected: {
        withoutGeometric: [
          {
            property: 'transform',
            runningOnCompositor: true
          }
        ],
        withGeometric: [
          {
            property: 'width',
            runningOnCompositor: false
          },
          {
            property: 'transform',
            runningOnCompositor: false,
            warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
          }
        ]
      }
    },
    {
      desc: 'translate',
      frames: {
        translate: ['0px', '100px']
      },
      expected: {
        withoutGeometric: [
          {
            property: 'translate',
            runningOnCompositor: true
          }
        ],
        withGeometric: [
          {
            property: 'width',
            runningOnCompositor: false
          },
          {
            property: 'translate',
            runningOnCompositor: false,
            warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
          }
        ]
      }
    },
    {
      desc: 'opacity and transform-like properties',
      frames: {
        opacity: [0, 1],
        transform: ['translate(0px)', 'translate(100px)'],
        translate: ['0px', '100px']
      },
      expected: {
        withoutGeometric: [
          {
            property: 'opacity',
            runningOnCompositor: true
          },
          {
            property: 'transform',
            runningOnCompositor: true
          },
          {
            property: 'translate',
            runningOnCompositor: true
          }
        ],
        withGeometric: [
          {
            property: 'width',
            runningOnCompositor: false
          },
          {
            property: 'opacity',
            runningOnCompositor: true
          },
          {
            property: 'transform',
            runningOnCompositor: false,
            warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
          },
          {
            property: 'translate',
            runningOnCompositor: false,
            warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
          }
        ]
      }
    },
  ].forEach(subtest => {
    promise_test(async t => {
      var animation = addDivAndAnimate(t, { class: 'compositable' },
                                       subtest.frames, 100 * MS_PER_SEC);
      await waitForPaints();

      // First, a transform animation is running on compositor.
      assert_animation_property_state_equals(
        animation.effect.getProperties(),
        subtest.expected.withoutGeometric);

      // Add a 'width' property.
      var keyframes = animation.effect.getKeyframes();

      keyframes[0].width = '100px';
      keyframes[1].width = '200px';

      animation.effect.setKeyframes(keyframes);
      await waitForFrame();

      // Now the transform animation is not running on compositor because of
      // the 'width' property.
      assert_animation_property_state_equals(
        animation.effect.getProperties(),
        subtest.expected.withGeometric);

      // Remove the 'width' property.
      var keyframes = animation.effect.getKeyframes();

      delete keyframes[0].width;
      delete keyframes[1].width;

      animation.effect.setKeyframes(keyframes);
      await waitForFrame();

      // Finally, the transform animation is running on compositor.
      assert_animation_property_state_equals(
        animation.effect.getProperties(),
        subtest.expected.withoutGeometric);
    }, 'An animation has: ' + subtest.desc);
  });
}

// Test that the expected set of geometric properties all block transform
// animations.
function testSetOfGeometricProperties() {
  const geometricProperties = [
    'width', 'height',
    'top', 'right', 'bottom', 'left',
    'margin-top', 'margin-right', 'margin-bottom', 'margin-left',
    'padding-top', 'padding-right', 'padding-bottom', 'padding-left'
  ];

  geometricProperties.forEach(property => {
    promise_test(async t => {
      const keyframes = {
        [propertyToIDL(property)]: [ '100px', '200px' ],
        transform: [ 'translate(0px)', 'translate(100px)' ]
      };
      var animation = addDivAndAnimate(t, { class: 'compositable' },
                                       keyframes, 100 * MS_PER_SEC);

      await waitForPaints();
      assert_animation_property_state_equals(
        animation.effect.getProperties(),
        [
          {
            property,
            runningOnCompositor: false
          },
          {
            property: 'transform',
            runningOnCompositor: false,
            warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
          }
        ]);
    }, `${property} is treated as a geometric property`);
  });
}

// Performance warning tests that set and clear a style property.
function testStyleChanges() {
  [
    {
      desc: 'preserve-3d transform',
      frames: {
        transform: ['translate(0px)', 'translate(100px)']
      },
      style: 'transform-style: preserve-3d',
      expected: [
        {
          property: 'transform',
          runningOnCompositor: true,
        }
      ]
    },
    {
      desc: 'preserve-3d translate',
      frames: {
        translate: ['0px', '100px']
      },
      style: 'transform-style: preserve-3d',
      expected: [
        {
          property: 'translate',
          runningOnCompositor: true,
        }
      ]
    },
    {
      desc: 'transform with backface-visibility:hidden',
      frames: {
        transform: ['translate(0px)', 'translate(100px)']
      },
      style: 'backface-visibility: hidden;',
      expected: [
        {
          property: 'transform',
          runningOnCompositor: false,
          warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
        }
      ]
    },
    {
      desc: 'translate with backface-visibility:hidden',
      frames: {
        translate: ['0px', '100px']
      },
      style: 'backface-visibility: hidden;',
      expected: [
        {
          property: 'translate',
          runningOnCompositor: false,
          warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
        }
      ]
    },
    {
      desc: 'opacity and transform-like properties with preserve-3d',
      frames: {
        opacity: [0, 1],
        transform: ['translate(0px)', 'translate(100px)'],
        translate: ['0px', '100px']
      },
      style: 'transform-style: preserve-3d',
      expected: [
        {
          property: 'opacity',
          runningOnCompositor: true
        },
        {
          property: 'transform',
          runningOnCompositor: true,
        },
        {
          property: 'translate',
          runningOnCompositor: true,
        }
      ]
    },
    {
      desc: 'opacity and transform-like properties with ' +
            'backface-visibility:hidden',
      frames: {
        opacity: [0, 1],
        transform: ['translate(0px)', 'translate(100px)'],
        translate: ['0px', '100px']
      },
      style: 'backface-visibility: hidden;',
      expected: [
        {
          property: 'opacity',
          runningOnCompositor: true
        },
        {
          property: 'transform',
          runningOnCompositor: false,
          warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
        },
        {
          property: 'translate',
          runningOnCompositor: false,
          warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
        }
      ]
    },
  ].forEach(subtest => {
    promise_test(async t => {
      var animation = addDivAndAnimate(t, { class: 'compositable' },
                                       subtest.frames, 100 * MS_PER_SEC);
      await waitForPaints();
      assert_all_properties_running_on_compositor(
        animation.effect.getProperties(),
        subtest.expected);
      animation.effect.target.style = subtest.style;
      await waitForFrame();

      assert_animation_property_state_equals(
        animation.effect.getProperties(),
        subtest.expected);
      animation.effect.target.style = '';
      await waitForFrame();

      assert_all_properties_running_on_compositor(
        animation.effect.getProperties(),
        subtest.expected);
    }, subtest.desc);
  });
}

// Performance warning tests that set and clear the id property
function testIdChanges() {
  [
    {
      desc: 'moz-element referencing a transform',
      frames: {
        transform: ['translate(0px)', 'translate(100px)']
      },
      id: 'transformed',
      createelement: 'width:100px; height:100px; background: -moz-element(#transformed)',
      expected: [
        {
          property: 'transform',
          runningOnCompositor: false,
          warning: 'CompositorAnimationWarningHasRenderingObserver'
        }
      ]
    },
    {
      desc: 'moz-element referencing a translate',
      frames: {
        translate: ['0px', '100px']
      },
      id: 'transformed',
      createelement: 'width:100px; height:100px; background: -moz-element(#transformed)',
      expected: [
        {
          property: 'translate',
          runningOnCompositor: false,
          warning: 'CompositorAnimationWarningHasRenderingObserver'
        }
      ]
    },
    {
      desc: 'moz-element referencing a translate and transform',
      frames: {
        transform: ['translate(0px)', 'translate(100px)'],
        translate: ['0px', '100px']
      },
      id: 'transformed',
      createelement: 'width:100px; height:100px; background: -moz-element(#transformed)',
      expected: [
        {
          property: 'translate',
          runningOnCompositor: false,
          warning: 'CompositorAnimationWarningHasRenderingObserver'
        },
        {
          property: 'transform',
          runningOnCompositor: false,
          warning: 'CompositorAnimationWarningHasRenderingObserver'
        }
      ]
    },
  ].forEach(subtest => {
    promise_test(async t => {
      if (subtest.createelement) {
        addDiv(t, { style: subtest.createelement });
      }

      var animation = addDivAndAnimate(t, { class: 'compositable' },
                                       subtest.frames, 100 * MS_PER_SEC);
      await waitForPaints();

      assert_all_properties_running_on_compositor(
        animation.effect.getProperties(),
        subtest.expected);
      animation.effect.target.id = subtest.id;
      await waitForFrame();

      assert_animation_property_state_equals(
        animation.effect.getProperties(),
        subtest.expected);
      animation.effect.target.id = '';
      await waitForFrame();

      assert_all_properties_running_on_compositor(
        animation.effect.getProperties(),
        subtest.expected);
    }, subtest.desc);
  });
}

function testMultipleAnimations() {
  [
    {
      desc: 'opacity and transform-like properties with preserve-3d',
      style: 'transform-style: preserve-3d',
      animations: [
        {
          frames: {
            transform: ['translate(0px)', 'translate(100px)']
          },
          expected: [
            {
              property: 'transform',
              runningOnCompositor: true,
            }
          ]
        },
        {
          frames: {
            translate: ['0px', '100px']
          },
          expected: [
            {
              property: 'translate',
              runningOnCompositor: true,
            }
          ]
        },
        {
          frames: {
            opacity: [0, 1]
          },
          expected: [
            {
              property: 'opacity',
              runningOnCompositor: true,
            }
          ]
        }
      ],
    },
    {
      desc: 'opacity and transform-like properties with ' +
            'backface-visibility:hidden',
      style: 'backface-visibility: hidden;',
      animations: [
        {
          frames: {
            transform: ['translate(0px)', 'translate(100px)']
          },
          expected: [
            {
              property: 'transform',
              runningOnCompositor: false,
              warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
            }
          ]
        },
        {
          frames: {
            translate: ['0px', '100px']
          },
          expected: [
            {
              property: 'translate',
              runningOnCompositor: false,
              warning: 'CompositorAnimationWarningTransformBackfaceVisibilityHidden'
            }
          ]
        },
        {
          frames: {
            opacity: [0, 1]
          },
          expected: [
            {
              property: 'opacity',
              runningOnCompositor: true,
            }
          ]
        }
      ],
    },
  ].forEach(subtest => {
    promise_test(async t => {
      var div = addDiv(t, { class: 'compositable' });
      var animations = subtest.animations.map(anim => {
        var animation = div.animate(anim.frames, 100 * MS_PER_SEC);

        // Bind expected values to animation object.
        animation.expected = anim.expected;
        return animation;
      });
      await waitForPaints();

      animations.forEach(anim => {
        assert_all_properties_running_on_compositor(
          anim.effect.getProperties(),
          anim.expected);
      });
      div.style = subtest.style;
      await waitForFrame();

      animations.forEach(anim => {
        assert_animation_property_state_equals(
          anim.effect.getProperties(),
          anim.expected);
      });
      div.style = '';
      await waitForFrame();

      animations.forEach(anim => {
        assert_all_properties_running_on_compositor(
          anim.effect.getProperties(),
          anim.expected);
      });
    }, 'Multiple animations: ' + subtest.desc);
  });
}

// Test adding/removing a 'width' keyframe on the same animation object, where
// multiple animation objects belong to the same element.
// The 'width' property is added to animations[1].
function testMultipleAnimationsWithGeometricKeyframes() {
  [
    {
      desc: 'transform and opacity with geometric keyframes',
      animations: [
        {
          frames: {
            transform: ['translate(0px)', 'translate(100px)']
          },
          expected: {
            withoutGeometric: [
              {
                property: 'transform',
                runningOnCompositor: true
              }
            ],
            withGeometric: [
              {
                property: 'transform',
                runningOnCompositor: false,
                warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
              }
            ]
          }
        },
        {
          frames: {
            opacity: [0, 1]
          },
          expected: {
            withoutGeometric: [
              {
                property: 'opacity',
                runningOnCompositor: true,
              }
            ],
            withGeometric: [
              {
                property: 'width',
                runningOnCompositor: false,
              },
              {
                property: 'opacity',
                runningOnCompositor: true,
              }
            ]
          }
        }
      ],
    },
    {
      desc: 'opacity and transform with geometric keyframes',
      animations: [
        {
          frames: {
            opacity: [0, 1]
          },
          expected: {
            withoutGeometric: [
              {
                property: 'opacity',
                runningOnCompositor: true,
              }
            ],
            withGeometric: [
              {
                property: 'opacity',
                runningOnCompositor: true,
              }
            ]
          }
        },
        {
          frames: {
            transform: ['translate(0px)', 'translate(100px)']
          },
          expected: {
            withoutGeometric: [
              {
                property: 'transform',
                runningOnCompositor: true
              }
            ],
            withGeometric: [
              {
                property: 'width',
                runningOnCompositor: false,
              },
              {
                property: 'transform',
                runningOnCompositor: false,
                warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
              }
            ]
          }
        }
      ]
    },
    {
      desc: 'opacity and translate with geometric keyframes',
      animations: [
        {
          frames: {
            opacity: [0, 1]
          },
          expected: {
            withoutGeometric: [
              {
                property: 'opacity',
                runningOnCompositor: true,
              }
            ],
            withGeometric: [
              {
                property: 'opacity',
                runningOnCompositor: true,
              }
            ]
          }
        },
        {
          frames: {
            translate: ['0px', '100px']
          },
          expected: {
            withoutGeometric: [
              {
                property: 'translate',
                runningOnCompositor: true
              }
            ],
            withGeometric: [
              {
                property: 'width',
                runningOnCompositor: false,
              },
              {
                property: 'translate',
                runningOnCompositor: false,
                warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
              }
            ]
          }
        }
      ]
    },
  ].forEach(subtest => {
    promise_test(async t => {
      var div = addDiv(t, { class: 'compositable' });
      var animations = subtest.animations.map(anim => {
        var animation = div.animate(anim.frames, 100 * MS_PER_SEC);

        // Bind expected values to animation object.
        animation.expected = anim.expected;
        return animation;
      });
      await waitForPaints();
      // First, all animations are running on compositor.
      animations.forEach(anim => {
        assert_animation_property_state_equals(
          anim.effect.getProperties(),
          anim.expected.withoutGeometric);
      });

      // Add a 'width' property to animations[1].
      var keyframes = animations[1].effect.getKeyframes();

      keyframes[0].width = '100px';
      keyframes[1].width = '200px';

      animations[1].effect.setKeyframes(keyframes);
      await waitForFrame();

      // Now the transform animation is not running on compositor because of
      // the 'width' property.
      animations.forEach(anim => {
        assert_animation_property_state_equals(
          anim.effect.getProperties(),
          anim.expected.withGeometric);
      });

      // Remove the 'width' property from animations[1].
      var keyframes = animations[1].effect.getKeyframes();

      delete keyframes[0].width;
      delete keyframes[1].width;

      animations[1].effect.setKeyframes(keyframes);
      await waitForFrame();

      // Finally, all animations are running on compositor.
      animations.forEach(anim => {
        assert_animation_property_state_equals(
          anim.effect.getProperties(),
          anim.expected.withoutGeometric);
      });
    }, 'Multiple animations with geometric property: ' + subtest.desc);
  });
}

// Tests adding/removing 'width' animation on the same element which has async
// animations.
function testMultipleAnimationsWithGeometricAnimations() {
  [
    {
      desc: 'transform',
      animations: [
        {
          frames: {
            transform: ['translate(0px)', 'translate(100px)']
          },
          expected: [
            {
              property: 'transform',
              runningOnCompositor: false,
              warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
            }
          ]
        },
      ]
    },
    {
      desc: 'translate',
      animations: [
        {
          frames: {
            translate: ['0px', '100px']
          },
          expected: [
            {
              property: 'translate',
              runningOnCompositor: false,
              warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
            }
          ]
        },
      ]
    },
    {
      desc: 'opacity',
      animations: [
        {
          frames: {
            opacity: [0, 1]
          },
          expected: [
            {
              property: 'opacity',
              runningOnCompositor: true
            }
          ]
        },
      ]
    },
    {
      desc: 'opacity, transform, and translate',
      animations: [
        {
          frames: {
            transform: ['translate(0px)', 'translate(100px)']
          },
          expected: [
            {
              property: 'transform',
              runningOnCompositor: false,
              warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
            }
          ]
        },
        {
          frames: {
            translate: ['0px', '100px']
          },
          expected: [
            {
              property: 'translate',
              runningOnCompositor: false,
              warning: 'CompositorAnimationWarningTransformWithGeometricProperties'
            }
          ]
        },
        {
          frames: {
            opacity: [0, 1]
          },
          expected: [
            {
              property: 'opacity',
              runningOnCompositor: true,
            }
          ]
        }
      ],
    },
  ].forEach(subtest => {
    promise_test(async t => {
      var div = addDiv(t, { class: 'compositable' });
      var animations = subtest.animations.map(anim => {
        var animation = div.animate(anim.frames, 100 * MS_PER_SEC);

        // Bind expected values to animation object.
        animation.expected = anim.expected;
        return animation;
      });

      var widthAnimation;

      await waitForPaints();
      animations.forEach(anim => {
        assert_all_properties_running_on_compositor(
          anim.effect.getProperties(),
          anim.expected);
      });

      // Append 'width' animation on the same element.
      widthAnimation = div.animate({ width: ['100px', '200px'] },
                                   100 * MS_PER_SEC);
      await waitForFrame();

      // Now transform animations are not running on compositor because of
      // the 'width' animation.
      animations.forEach(anim => {
        assert_animation_property_state_equals(
          anim.effect.getProperties(),
          anim.expected);
      });
      // Remove the 'width' animation.
      widthAnimation.cancel();
      await waitForFrame();

      // Now all animations are running on compositor.
      animations.forEach(anim => {
        assert_all_properties_running_on_compositor(
          anim.effect.getProperties(),
          anim.expected);
      });
    }, 'Multiple async animations and geometric animation: ' + subtest.desc);
  });
}

function testSmallElements() {
  [
    {
      desc: 'opacity on small element',
      frames: {
        opacity: [0, 1]
      },
      style: { style: 'width: 8px; height: 8px; background-color: red;' +
                      // We need to set transform here to try creating an
                      // individual frame for this opacity element.
                      // Without this, this small element is created on the same
                      // nsIFrame of mochitest iframe, i.e. the document which are
                      // running this test, as a result the layer corresponding
                      // to the frame is sent to compositor.
                      'transform: translateX(100px);' },
      expected: [
        {
          property: 'opacity',
          runningOnCompositor: true
        }
      ]
    },
    {
      desc: 'transform on small element',
      frames: {
        transform: ['translate(0px)', 'translate(100px)']
      },
      style: { style: 'width: 8px; height: 8px; background-color: red;' },
      expected: [
        {
          property: 'transform',
          runningOnCompositor: true
        }
      ]
    },
    {
      desc: 'translate on small element',
      frames: {
        translate: ['0px', '100px']
      },
      style: { style: 'width: 8px; height: 8px; background-color: red;' },
      expected: [
        {
          property: 'translate',
          runningOnCompositor: true
        }
      ]
    },
  ].forEach(subtest => {
    promise_test(async t => {
    var div = addDiv(t, subtest.style);
    var animation = div.animate(subtest.frames, 100 * MS_PER_SEC);
    await waitForPaints();

    assert_animation_property_state_equals(
      animation.effect.getProperties(),
      subtest.expected);
    }, subtest.desc);
  });
}

function testSynchronizedAnimations() {
  promise_test(async t => {
    const elemA = addDiv(t, { class: 'compositable' });
    const elemB = addDiv(t, { class: 'compositable' });

    const animA = elemA.animate({ transform: [ 'translate(0px)',
                                               'translate(100px)' ] },
                                100 * MS_PER_SEC);
    const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
                                100 * MS_PER_SEC);

    await waitForPaints();

    assert_animation_property_state_equals(
      animA.effect.getProperties(),
      [ { property: 'transform',
          runningOnCompositor: false,
          warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
      } ]);
  }, 'Animations created within the same tick are synchronized'
     + ' (compositor animation created first)');

  promise_test(async t => {
    const elemA = addDiv(t, { class: 'compositable' });
    const elemB = addDiv(t, { class: 'compositable' });
    const elemC = addDiv(t, { class: 'compositable' });

    const animA = elemA.animate({ transform: [ 'translate(0px)',
                                               'translate(100px)' ] },
                                100 * MS_PER_SEC);
    const animB = elemB.animate({ translate: [ '0px', '100px' ] },
                                100 * MS_PER_SEC);
    const animC = elemC.animate({ marginLeft: [ '0px', '100px' ] },
                                100 * MS_PER_SEC);

    await waitForPaints();

    assert_animation_property_state_equals(
      animA.effect.getProperties(),
      [
        { property: 'transform',
          runningOnCompositor: false,
          warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
      } ]);
    assert_animation_property_state_equals(
      animB.effect.getProperties(),
      [
        { property: 'translate',
          runningOnCompositor: false,
          warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
      } ]);
  }, 'Animations created within the same tick are synchronized'
     + ' (compositor animation created first/second)');

  promise_test(async t => {
    const elemA = addDiv(t, { class: 'compositable' });
    const elemB = addDiv(t, { class: 'compositable' });
    const elemC = addDiv(t, { class: 'compositable' });

    const animA = elemA.animate({ marginLeft: [ '0px', '100px' ] },
                                100 * MS_PER_SEC);
    const animB = elemB.animate({ transform: [ 'translate(0px)',
                                               'translate(100px)' ] },
                                100 * MS_PER_SEC);
    const animC = elemC.animate({ translate: [ '0px', '100px' ] },
                                100 * MS_PER_SEC);

    await waitForPaints();

    assert_animation_property_state_equals(
      animB.effect.getProperties(),
      [ { property: 'transform',
          runningOnCompositor: false,
          warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
      } ]);
    assert_animation_property_state_equals(
      animC.effect.getProperties(),
      [
        { property: 'translate',
          runningOnCompositor: false,
          warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
      } ]);
  }, 'Animations created within the same tick are synchronized'
     + ' (compositor animation created second/third)');

  promise_test(async t => {
    const attrs = { class: 'compositable',
                    style: 'transition: all 100s' };
    const elemA = addDiv(t, attrs);
    const elemB = addDiv(t, attrs);
    elemA.style.transform = 'translate(0px)';
    elemB.style.marginLeft = '0px';
    getComputedStyle(elemA).transform;
    getComputedStyle(elemB).marginLeft;

    // Generally the sequence of steps is as follows:
    //
    //   Tick -> requestAnimationFrame -> Style -> Paint -> Events (-> Tick...)
    //
    // In this test we want to set up two transitions during the "Events"
    // stage but only flush style for one such that the second one is actually
    // generated during the "Style" stage of the *next* tick.
    //
    // Web content often generates transitions in this way (that is, it doesn't
    // pay regard to when style is flushed and nor should it). However, we
    // still want transitions generated in this way to be synchronized.
    let timeForFirstFrame;
    await waitForIdle();

    timeForFirstFrame = document.timeline.currentTime;
    elemA.style.transform = 'translate(100px)';
    // Flush style to trigger first transition
    getComputedStyle(elemA).transform;
    elemB.style.marginLeft = '100px';
    // DON'T flush style here (this includes calling getAnimations!)
    await waitForFrame();

    assert_not_equals(timeForFirstFrame, document.timeline.currentTime,
                      'Should be on the other side of a tick');
    // Wait another tick so we can let the transition be started
    // by regular style resolution.
    await waitForFrame();

    const transitionA = elemA.getAnimations()[0];
    assert_animation_property_state_equals(
      transitionA.effect.getProperties(),
      [ { property: 'transform',
          runningOnCompositor: false,
          warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
      } ]);
  }, 'Transitions created before and after a tick are synchronized');

  promise_test(async t => {
    const elemA = addDiv(t, { class: 'compositable' });
    const elemB = addDiv(t, { class: 'compositable' });

    const animA = elemA.animate({ transform: [ 'translate(0px)',
                                               'translate(100px)' ],
                                  opacity: [ 0, 1 ] },
                                100 * MS_PER_SEC);
    const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
                                100 * MS_PER_SEC);

    await waitForPaints();

    assert_animation_property_state_equals(
      animA.effect.getProperties(),
      [ { property: 'transform',
          runningOnCompositor: false,
          warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
        },
        { property: 'opacity',
          runningOnCompositor: true
        } ]);
  }, 'Opacity animations on the same element continue running on the'
     + ' compositor when transform animations are synchronized with geometric'
     + ' animations');

  promise_test(async t => {
    const transitionElem = addDiv(t, {
      style: 'margin-left: 0px; transition: margin-left 100s',
    });
    getComputedStyle(transitionElem).marginLeft;

    await waitForFrame();

    transitionElem.style.marginLeft = '100px';
    const cssTransition = transitionElem.getAnimations()[0];

    const animationElem = addDiv(t, {
      class: 'compositable',
      style: 'animation: translate 100s',
    });
    const cssAnimation = animationElem.getAnimations()[0];

    await Promise.all([cssTransition.ready, cssAnimation.ready]);

    assert_animation_property_state_equals(cssAnimation.effect.getProperties(),
      [{ property: 'transform',
         runningOnCompositor: true }]);
  }, 'CSS Animations are NOT synchronized with CSS Transitions');

  promise_test(async t => {
    const elemA = addDiv(t, { class: 'compositable' });
    const elemB = addDiv(t, { class: 'compositable' });

    const animA = elemA.animate({ marginLeft: [ '0px', '100px' ] },
                                100 * MS_PER_SEC);
    await waitForPaints();

    let animB = elemB.animate({ transform: [ 'translate(0px)',
                                             'translate(100px)' ] },
                              100 * MS_PER_SEC);
    await animB.ready;

    assert_animation_property_state_equals(
      animB.effect.getProperties(),
      [ { property: 'transform',
          runningOnCompositor: true } ]);
  }, 'Transform animations are NOT synchronized with geometric animations'
     + ' started in the previous frame');

  promise_test(async t => {
    const elemA = addDiv(t, { class: 'compositable' });
    const elemB = addDiv(t, { class: 'compositable' });

    const animA = elemA.animate({ transform: [ 'translate(0px)',
                                               'translate(100px)' ] },
                                100 * MS_PER_SEC);
    await waitForPaints();

    let animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
                              100 * MS_PER_SEC);
    await animB.ready;

    assert_animation_property_state_equals(
      animA.effect.getProperties(),
      [ { property: 'transform',
          runningOnCompositor: true } ]);
  }, 'Transform animations are NOT synchronized with geometric animations'
     + ' started in the next frame');

  promise_test(async t => {
    const elemA = addDiv(t, { class: 'compositable' });
    const elemB = addDiv(t, { class: 'compositable' });

    const animA = elemA.animate({ transform: [ 'translate(0px)',
                                               'translate(100px)' ] },
                                100 * MS_PER_SEC);
    const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
                                100 * MS_PER_SEC);
    animB.pause();

    await waitForPaints();

    assert_animation_property_state_equals(
      animA.effect.getProperties(),
      [ { property: 'transform', runningOnCompositor: true } ]);
  }, 'Paused animations are not synchronized');

  promise_test(async t => {
    const elemA = addDiv(t, { class: 'compositable' });
    const elemB = addDiv(t, { class: 'compositable' });

    const animA = elemA.animate({ transform: [ 'translate(0px)',
                                               'translate(100px)' ] },
                                100 * MS_PER_SEC);
    const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
                                100 * MS_PER_SEC);

    // Seek one of the animations so that their start times will differ
    animA.currentTime = 5000;

    await waitForPaints();

    assert_not_equals(animA.startTime, animB.startTime,
                      'Animations should have different start times');
    assert_animation_property_state_equals(
      animA.effect.getProperties(),
      [ { property: 'transform',
          runningOnCompositor: false,
          warning: 'CompositorAnimationWarningTransformWithSyncGeometricAnimations'
      } ]);
  }, 'Animations are synchronized based on when they are started'
     + ' and NOT their start time');

  promise_test(async t => {
    const elemA = addDiv(t, { class: 'compositable' });
    const elemB = addDiv(t, { class: 'compositable' });

    const animA = elemA.animate({ transform: [ 'translate(0px)',
                                               'translate(100px)' ] },
                                100 * MS_PER_SEC);
    const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
                                100 * MS_PER_SEC);

    await waitForPaints();

    assert_animation_property_state_equals(
      animA.effect.getProperties(),
      [ { property: 'transform',
          runningOnCompositor: false } ]);
    // Restart animation
    animA.pause();
    animA.play();
    await animA.ready;

    assert_animation_property_state_equals(
      animA.effect.getProperties(),
      [ { property: 'transform',
          runningOnCompositor: true } ]);
  }, 'An initially synchronized animation may be unsynchronized if restarted');

  promise_test(async t => {
    const elemA = addDiv(t, { class: 'compositable' });
    const elemB = addDiv(t, { class: 'compositable' });

    const animA = elemA.animate({ transform: [ 'translate(0px)',
                                               'translate(100px)' ] },
                                100 * MS_PER_SEC);
    const animB = elemB.animate({ marginLeft: [ '0px', '100px' ] },
                                100 * MS_PER_SEC);

    // Clear target effect
    animB.effect.target = null;

    await waitForPaints();

    assert_animation_property_state_equals(
      animA.effect.getProperties(),
      [ { property: 'transform',
          runningOnCompositor: true } ]);
  }, 'A geometric animation with no target element is not synchronized');
}

function testTooLargeFrame() {
  [
    {
      property: 'transform',
      frames: { transform: ['translate(0px)', 'translate(100px)'] },
    },
    {
      property: 'translate',
      frames: { translate: ['0px', '100px'] },
    },
  ].forEach(subtest => {
    promise_test(async t => {
      var animation = addDivAndAnimate(t,
                                       { class: 'compositable' },
                                       subtest.frames,
                                       100 * MS_PER_SEC);
      await waitForPaints();

      assert_animation_property_state_equals(
        animation.effect.getProperties(),
        [ { property: subtest.property, runningOnCompositor: true } ]);
      animation.effect.target.style = 'width: 10000px; height: 10000px';
      await waitForFrame();

      // viewport depends on test environment.
      var expectedWarning = new RegExp(
        "Animation cannot be run on the compositor because the area of the frame " +
        "\\(\\d+\\) is too large relative to the viewport " +
        "\\(larger than \\d+\\)");
      assert_animation_property_state_equals(
        animation.effect.getProperties(),
        [ {
          property: subtest.property,
          runningOnCompositor: false,
          warning: expectedWarning
        } ]);
      animation.effect.target.style = 'width: 100px; height: 100px';
      await waitForFrame();

      // With WebRender we appear to stick to the previous layerization decision
      // after changing the bounds back to a smaller object.
      const isWebRender =
        SpecialPowers.DOMWindowUtils.layerManagerType.startsWith('WebRender');
      assert_animation_property_state_equals(
        animation.effect.getProperties(),
        [ { property: subtest.property, runningOnCompositor: !isWebRender } ]);
    }, subtest.property + ' on too big element - area');

    promise_test(async t => {
      var animation = addDivAndAnimate(t,
                                       { class: 'compositable' },
                                       subtest.frames,
                                       100 * MS_PER_SEC);
      await waitForPaints();

      assert_animation_property_state_equals(
        animation.effect.getProperties(),
        [ { property: subtest.property, runningOnCompositor: true } ]);
      animation.effect.target.style = 'width: 20000px; height: 1px';
      await waitForFrame();

      // viewport depends on test environment.
      var expectedWarning = new RegExp(
        "Animation cannot be run on the compositor because the frame size " +
        "\\(20000, 1\\) is too large relative to the viewport " +
        "\\(larger than \\(\\d+, \\d+\\)\\) or larger than the " +
        "maximum allowed value \\(\\d+, \\d+\\)");
      assert_animation_property_state_equals(
        animation.effect.getProperties(),
        [ {
          property: subtest.property,
          runningOnCompositor: false,
          warning: expectedWarning
        } ]);
      animation.effect.target.style = 'width: 100px; height: 100px';
      await waitForFrame();

      const isWebRender =
        SpecialPowers.DOMWindowUtils.layerManagerType.startsWith('WebRender');
      assert_animation_property_state_equals(
        animation.effect.getProperties(),
        [ { property: subtest.property, runningOnCompositor: !isWebRender } ]);
    }, subtest.property + ' on too big element - dimensions');
  });
}

function testTransformSVG() {
  [
    {
      property: 'transform',
      frames: { transform: ['translate(0px)', 'translate(100px)'] },
    },
    {
      property: 'translate',
      frames: { translate: ['0px', '100px'] },
    },
    {
      property: 'rotate',
      frames: { rotate: ['0deg', '45deg'] },
    },
    {
      property: 'scale',
      frames: { scale: ['1', '2'] },
    },
  ].forEach(subtest => {
    promise_test(async t => {
      var svg  = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
      svg.setAttribute('width', '100');
      svg.setAttribute('height', '100');
      var rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
      rect.setAttribute('width', '100');
      rect.setAttribute('height', '100');
      rect.setAttribute('fill', 'red');
      svg.appendChild(rect);
      document.body.appendChild(svg);
      t.add_cleanup(() => {
        svg.remove();
      });

      var animation = svg.animate(subtest.frames, 100 * MS_PER_SEC);
      await waitForPaints();

      assert_animation_property_state_equals(
        animation.effect.getProperties(),
        [ { property: subtest.property, runningOnCompositor: true } ]);
      svg.setAttribute('transform', 'translate(10, 20)');
      await waitForFrame();

      assert_animation_property_state_equals(
        animation.effect.getProperties(),
        [ {
          property: subtest.property,
          runningOnCompositor: false,
          warning: 'CompositorAnimationWarningTransformSVG'
        } ]);
      svg.removeAttribute('transform');
      await waitForFrame();

      assert_animation_property_state_equals(
        animation.effect.getProperties(),
        [ { property: subtest.property, runningOnCompositor: true } ]);
    }, subtest.property + ' of nsIFrame with SVG transform');
  });
}

function testImportantRuleOverride() {
  promise_test(async t => {
    const elem = addDiv(t, { class: 'compositable' });
    const anim = elem.animate({ translate: [ '0px', '100px' ],
                                rotate: ['0deg', '90deg'] },
                                100 * MS_PER_SEC);

    await waitForAnimationReadyToRestyle(anim);
    await waitForPaints();

    assert_animation_property_state_equals(
      anim.effect.getProperties(),
      [ { property: 'translate', runningOnCompositor: true },
        { property: 'rotate', runningOnCompositor: true } ]
    );

    elem.style.setProperty('rotate', '45deg', 'important');
    getComputedStyle(elem).rotate;

    await waitForFrame();

    assert_animation_property_state_equals(
      anim.effect.getProperties(),
      [
        {
          property: 'translate',
          runningOnCompositor: false,
          warning:
            'CompositorAnimationWarningTransformIsBlockedByImportantRules'
        },
        {
          property: 'rotate',
          runningOnCompositor: false,
          warning:
            'CompositorAnimationWarningTransformIsBlockedByImportantRules'
        },
      ]
    );
  }, 'The animations of transform-like properties are not running on the ' +
     'compositor because any of the properties has important rules');
}

function testCurrentColor() {
  if (SpecialPowers.DOMWindowUtils.layerManagerType.startsWith('WebRender')) {
    return; // skip this test until bug 1510030 landed.
  }
  promise_test(async t => {
    const animation = addDivAndAnimate(t, { class: 'compositable' },
                                       { backgroundColor: [ 'currentColor',
                                                            'red' ] },
                                       100 * MS_PER_SEC);
    await waitForPaints();

    assert_animation_property_state_equals(
      animation.effect.getProperties(),
      [ { property: 'background-color',
          runningOnCompositor: false,
          warning: 'CompositorAnimationWarningHasCurrentColor'
      } ]);
  }, 'Background color animations with `current-color` don\'t run on the '
     + 'compositor');
}

function start() {
  var bundleService = SpecialPowers.Cc['@mozilla.org/intl/stringbundle;1']
    .getService(SpecialPowers.Ci.nsIStringBundleService);
  gStringBundle = bundleService
    .createBundle("chrome://global/locale/layout_errors.properties");

  testBasicOperation();
  testKeyframesWithGeometricProperties();
  testSetOfGeometricProperties();
  testStyleChanges();
  testIdChanges();
  testMultipleAnimations();
  testMultipleAnimationsWithGeometricKeyframes();
  testMultipleAnimationsWithGeometricAnimations();
  testSmallElements();
  testSynchronizedAnimations();
  testTooLargeFrame();
  testTransformSVG();
  testImportantRuleOverride();
  testCurrentColor();

  promise_test(async t => {
    var div = addDiv(t, { class: 'compositable',
                          style: 'animation: fade 100s' });
    var cssAnimation = div.getAnimations()[0];
    var scriptAnimation = div.animate({ opacity: [ 1, 0 ] }, 100 * MS_PER_SEC);

    await waitForPaints();
    assert_animation_property_state_equals(
      cssAnimation.effect.getProperties(),
      [ { property: 'opacity', runningOnCompositor: true } ]);
    assert_animation_property_state_equals(
      scriptAnimation.effect.getProperties(),
      [ { property: 'opacity', runningOnCompositor: true } ]);
  }, 'overridden animation');

  done();
}

</script>

</body>
