<!DOCTYPE html>
<body>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<style>

div {
    position: absolute;
    left: 0;
    top: 0;
    width: 100px;
    height: 100px;
    background-color: black;
}

</style>
<script>

const createDiv = test => {
    const div = document.createElement("div");
    test.add_cleanup(() => div.remove());
    return document.body.appendChild(div);
}

const animationReadyToAnimateAccelerated = async animation => {
    await animation.ready;
    await new Promise(requestAnimationFrame);
    await new Promise(requestAnimationFrame);
    await new Promise(requestAnimationFrame);
}

const duration = 1000 * 1000; // 1000s.

promise_test(async test => {
    const target = createDiv(test);
    const animation = target.animate(
        { transform: "translateX(100px)" },
        { composite: "add", duration }
    );
    await animationReadyToAnimateAccelerated(animation);
    assert_equals(internals.acceleratedAnimationsForElement(target).length, 0);
}, "Setting 'composite' to 'add' should prevent acceleration");

promise_test(async test => {
    const target = createDiv(test);
    const animation = target.animate(
        [{ offset: 1, composite: "add", transform: "translateX(100px)" }],
        { duration }
    );
    await animationReadyToAnimateAccelerated(animation);
    assert_equals(internals.acceleratedAnimationsForElement(target).length, 0);
}, "Setting 'composite' to 'add' on a keyframe should prevent acceleration");

promise_test(async test => {
    const target = createDiv(test);
    const animations = [];
    // First add a replace animation that could be accelerated by itself.
    animations.push(target.animate(
        { transform: "translateX(100px)" },
        { duration }
    ));
    // Then, add a composing animation that prevents the whole stack to be accelerated.
    animations.push(target.animate(
        { transform: "translateY(100px)" },
        { composite: "add", duration }
    ));
    await Promise.all(animations.map(animation => animationReadyToAnimateAccelerated(animation)));
    assert_equals(internals.acceleratedAnimationsForElement(target).length, 0);
}, "Setting 'composite' to 'add' on an animation further up the stack should prevent lower animations in the stack from being accelerated");

promise_test(async test => {
    const target = createDiv(test);
    const animations = [];
    // First add a composing animation that cannot be run accelerated on its own.
    animations.push(target.animate(
        { transform: "translateX(100px)" },
        { composite: "add", duration }
    ));
    // Then, add a replace animation that should be accelerated since it completely obliterates
    // the previous composing animation.
    animations.push(target.animate(
        { transform: ["none", "translateY(100px)"] },
        { duration }
    ));
    await Promise.all(animations.map(animation => animationReadyToAnimateAccelerated(animation)));
    assert_equals(internals.acceleratedAnimationsForElement(target).length, 0);
}, "Setting 'composite' to 'add' on an animation lower down the stack should allow replace animations further up the stack to be accelerated");

promise_test(async test => {
    const target = createDiv(test);
    const animations = [];
    // First add a composing animation that cannot be run accelerated on its own.
    animations.push(target.animate(
        { transform: "translateX(100px)" },
        { composite: "add", duration }
    ));
    // Then, add a replace animation that should be accelerated except it can't because
    // it contains an implicit keyframe derived from the output of the previous animation.
    animations.push(target.animate(
        { transform: "translateX(100px)" },
        { duration }
    ));
    await Promise.all(animations.map(animation => animationReadyToAnimateAccelerated(animation)));
    assert_equals(internals.acceleratedAnimationsForElement(target).length, 0);
}, "Setting 'composite' to 'add' on an animation lower down the stack should prevent replace animations with an implicity keyframe further up the stack to be accelerated");

promise_test(async test => {
    const target = createDiv(test);
    const animations = [];
    // First add a composing animation that cannot be run accelerated on its own.
    animations.push(target.animate(
        { marginLeft: "100px" },
        { composite: "add", duration }
    ));
    // Then, add a replace animation with an implicit keyframes that can be accelerated
    // since there are no other composing animations below it in the stack that target
    // that property.
    animations.push(target.animate(
        { transform: "translateX(100px)" },
        { duration }
    ));
    await Promise.all(animations.map(animation => animationReadyToAnimateAccelerated(animation)));
    assert_equals(internals.acceleratedAnimationsForElement(target).length, 1);
}, "Setting 'composite' to 'add' on an animation lower down the stack targeting a property that isn't accelerated should allow replace animations with implicit keyframes further up the stack to be accelerated");

// The following tests check the dynamic interruption and/or start of accelerated animations
// after changes made to other effects in the stack.

promise_test(async test => {
    const target = createDiv(test);
    const animation = target.animate({ transform: "translateX(100px)" }, duration);

    await animationReadyToAnimateAccelerated(animation);
    assert_equals(internals.acceleratedAnimationsForElement(target).length, 1);

    // Setting the effect composition to "add" means that the animation should
    // no longer be accelerated.
    animation.effect.composite = "add";
    await animationReadyToAnimateAccelerated(animation);
    assert_equals(internals.acceleratedAnimationsForElement(target).length, 0);

    // Resetting the effect composition to "replace" means that the animation
    // should be accelerated again.
    animation.effect.composite = "replace";
    await animationReadyToAnimateAccelerated(animation);
    assert_equals(internals.acceleratedAnimationsForElement(target).length, 1);
}, "Dynamically setting 'composite' on an effect should toggle acceleration");

promise_test(async test => {
    const keyframes = composite => [{ offset: 1, composite, transform: "translateX(100px)" }];

    const target = createDiv(test);
    const animation = target.animate(keyframes("replace"), duration);
    await animationReadyToAnimateAccelerated(animation);
    assert_equals(internals.acceleratedAnimationsForElement(target).length, 1);

    // Setting the keyframe composition to "add" means that the animation should
    // no longer be accelerated.
    animation.effect.setKeyframes(keyframes("add"));
    await animationReadyToAnimateAccelerated(animation);
    assert_equals(internals.acceleratedAnimationsForElement(target).length, 0);

    // Resetting the keyframe composition to "replace" means that the animation
    // should be accelerated again.
    animation.effect.setKeyframes(keyframes("replace"));
    await animationReadyToAnimateAccelerated(animation);
    assert_equals(internals.acceleratedAnimationsForElement(target).length, 1);
}, "Dynamically setting 'composite' on a keyframe should toggle acceleration");

promise_test(async test => {
    const target = createDiv(test);
    const animations = [];

    animations.push(target.animate({ transform: "translateX(100px)" }, duration));
    animations.push(target.animate({ transform: "translateY(100px)" }, duration));

    await Promise.all(animations.map(animation => animationReadyToAnimateAccelerated(animation)));
    assert_equals(internals.acceleratedAnimationsForElement(target).length, 2, "Step 1");

    // Make the second animation a composing animation. This will prevent the first animation
    // from running accelerated as well.
    animations[1].effect.composite = "add";
    await Promise.all(animations.map(animation => animationReadyToAnimateAccelerated(animation)));
    assert_equals(internals.acceleratedAnimationsForElement(target).length, 0, "Step 2");

    // Reset to the original state.
    animations[1].effect.composite = "replace";
    await Promise.all(animations.map(animation => animationReadyToAnimateAccelerated(animation)));
    assert_equals(internals.acceleratedAnimationsForElement(target).length, 2, "Step 3");
}, "Dynamically setting 'composite' to 'add' on an animation further up the stack should toggle acceleration on lower animations in the stack");

promise_test(async test => {
    const target = createDiv(test);

    const animations = [];
    animations.push(target.animate({ transform: "translateX(100px)" }, duration));
    animations.push(target.animate({ transform: ["none", "translateY(100px)"] }, duration));
    await Promise.all(animations.map(animation => animationReadyToAnimateAccelerated(animation)));
    assert_equals(internals.acceleratedAnimationsForElement(target).length, 2);

    // Make the first animation a composing animation. This will prevent the other animation from running accelerated.
    animations[0].effect.composite = "add";
    await Promise.all(animations.map(animation => animationReadyToAnimateAccelerated(animation)));
    assert_equals(internals.acceleratedAnimationsForElement(target).length, 0);

    // Reset to the original state.
    animations[0].effect.composite = "replace";
    await Promise.all(animations.map(animation => animationReadyToAnimateAccelerated(animation)));
    assert_equals(internals.acceleratedAnimationsForElement(target).length, 2);
}, "Dynamically setting 'composite' to 'add' on an animation lower down the stack should toggle acceleration but always allow replace animations further up the stack to be accelerated");

promise_test(async test => {
    const target = createDiv(test);
    const animations = [];

    animations.push(target.animate({ transform: "translateX(100px)" }, duration));
    animations.push(target.animate({ transform: "translateX(100px)" }, duration));
    await Promise.all(animations.map(animation => animationReadyToAnimateAccelerated(animation)));
    assert_equals(internals.acceleratedAnimationsForElement(target).length, 2);

    // Make the first animation a composing animation. This will prevent both animations
    // from running accelerated since the second animation uses the first animation as
    // input due to using an implicit 0% keyframe.
    animations[0].effect.composite = "add";
    await Promise.all(animations.map(animation => animationReadyToAnimateAccelerated(animation)));
    assert_equals(internals.acceleratedAnimationsForElement(target).length, 0);

    // Reset to the original state.
    animations[0].effect.composite = "replace";
    await Promise.all(animations.map(animation => animationReadyToAnimateAccelerated(animation)));
    assert_equals(internals.acceleratedAnimationsForElement(target).length, 2);
}, "Dynamically setting 'composite' to 'add' on an animation lower down the stack should toggle acceleration on replace animations with an implicity keyframe further up the stack");

promise_test(async test => {
    const target = createDiv(test);
    const animations = [];

    animations.push(target.animate({ marginLeft: "100px" }, duration));
    animations.push(target.animate({ transform: "translateX(100px)" }, duration));
    await Promise.all(animations.map(animation => animationReadyToAnimateAccelerated(animation)));
    assert_equals(internals.acceleratedAnimationsForElement(target).length, 1);

    // Make the first animation a composing animation. This should not prevent the other animation from running accelerated.
    animations[0].effect.composite = "add";
    await Promise.all(animations.map(animation => animationReadyToAnimateAccelerated(animation)));
    assert_equals(internals.acceleratedAnimationsForElement(target).length, 1);

    // Reset to the original state.
    animations[0].effect.composite = "replace";
    await Promise.all(animations.map(animation => animationReadyToAnimateAccelerated(animation)));
    assert_equals(internals.acceleratedAnimationsForElement(target).length, 1);
}, "Dynamically setting 'composite' to 'add' on an animation lower down the stack targeting a property that isn't accelerated shouldn't prevent acceleration of animations with implicit keyframes further up the stack to be accelerated");

promise_test(async test => {
    const target = createDiv(test);
    const animation = target.animate({ transform: "translateX(100px)" }, duration);

    await animationReadyToAnimateAccelerated(animation);
    assert_equals(internals.acceleratedAnimationsForElement(target).length, 1);

    const composingAnimation = target.animate(
        { transform: "translateY(100px)" },
        { composite: "add", duration }
    );

    await animationReadyToAnimateAccelerated(composingAnimation);
    assert_equals(internals.acceleratedAnimationsForElement(target).length, 0);
}, "Adding a composing effect on top of an existing replace effect should prevent both effects from running accelerated");

</script>
</body>
