blob: c7d170c625acc2f4e6b53e8ff90b3858f474cbcc [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/animation/keyframe_effect.h"
#include <memory>
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/bindings/core/v8/native_value_traits_impl.h"
#include "third_party/blink/renderer/bindings/core/v8/unrestricted_double_or_keyframe_effect_options.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_effect_timing.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_keyframe_effect_options.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_object_builder.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_optional_effect_timing.h"
#include "third_party/blink/renderer/core/animation/animation.h"
#include "third_party/blink/renderer/core/animation/animation_clock.h"
#include "third_party/blink/renderer/core/animation/animation_test_helpers.h"
#include "third_party/blink/renderer/core/animation/document_timeline.h"
#include "third_party/blink/renderer/core/animation/keyframe_effect_model.h"
#include "third_party/blink/renderer/core/animation/timing.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/node_computed_style.h"
#include "third_party/blink/renderer/core/style/computed_style.h"
#include "third_party/blink/renderer/core/testing/page_test_base.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
#include "v8/include/v8.h"
namespace blink {
#define EXPECT_TIMEDELTA(expected, observed) \
EXPECT_NEAR(expected.InMillisecondsF(), observed.InMillisecondsF(), \
Animation::kTimeToleranceMs)
using animation_test_helpers::SetV8ObjectPropertyAsNumber;
using animation_test_helpers::SetV8ObjectPropertyAsString;
class KeyframeEffectTest : public PageTestBase {
protected:
void SetUp() override {
PageTestBase::SetUp(IntSize());
element = GetDocument().CreateElementForBinding("foo");
GetDocument().documentElement()->AppendChild(element.Get());
}
KeyframeEffectModelBase* CreateEmptyEffectModel() {
return MakeGarbageCollected<StringKeyframeEffectModel>(
StringKeyframeVector());
}
// Returns a two-frame effect updated styles.
KeyframeEffect* GetTwoFrameEffect(const CSSPropertyID& property,
const String& value_a,
const String& value_b) {
StringKeyframeVector keyframes(2);
keyframes[0] = MakeGarbageCollected<StringKeyframe>();
keyframes[0]->SetOffset(0.0);
keyframes[0]->SetCSSPropertyValue(
property, value_a, SecureContextMode::kInsecureContext, nullptr);
keyframes[1] = MakeGarbageCollected<StringKeyframe>();
keyframes[1]->SetOffset(1.0);
keyframes[1]->SetCSSPropertyValue(
property, value_b, SecureContextMode::kInsecureContext, nullptr);
auto* model = MakeGarbageCollected<StringKeyframeEffectModel>(keyframes);
Timing timing;
auto* effect = MakeGarbageCollected<KeyframeEffect>(element, model, timing);
// Ensure GetCompositorKeyframeValue is updated which would normally happen
// when applying the animation styles.
UpdateAllLifecyclePhasesForTest();
model->SnapshotAllCompositorKeyframesIfNecessary(
*element, *element->GetComputedStyle(), nullptr);
return effect;
}
Persistent<Element> element;
};
class AnimationKeyframeEffectV8Test : public KeyframeEffectTest {
protected:
static KeyframeEffect* CreateAnimationFromTiming(
ScriptState* script_state,
Element* element,
const ScriptValue& keyframe_object,
double timing_input) {
NonThrowableExceptionState exception_state;
return KeyframeEffect::Create(
script_state, element, keyframe_object,
UnrestrictedDoubleOrKeyframeEffectOptions::FromUnrestrictedDouble(
timing_input),
exception_state);
}
static KeyframeEffect* CreateAnimationFromOption(
ScriptState* script_state,
Element* element,
const ScriptValue& keyframe_object,
const KeyframeEffectOptions* timing_input) {
NonThrowableExceptionState exception_state;
return KeyframeEffect::Create(
script_state, element, keyframe_object,
UnrestrictedDoubleOrKeyframeEffectOptions::FromKeyframeEffectOptions(
const_cast<KeyframeEffectOptions*>(timing_input)),
exception_state);
}
static KeyframeEffect* CreateAnimation(ScriptState* script_state,
Element* element,
const ScriptValue& keyframe_object) {
NonThrowableExceptionState exception_state;
return KeyframeEffect::Create(script_state, element, keyframe_object,
exception_state);
}
};
TEST_F(AnimationKeyframeEffectV8Test, CanCreateAnAnimation) {
V8TestingScope scope;
ScriptState* script_state = scope.GetScriptState();
NonThrowableExceptionState exception_state;
HeapVector<ScriptValue> blink_keyframes = {
V8ObjectBuilder(script_state)
.AddString("width", "100px")
.AddString("offset", "0")
.AddString("easing", "ease-in-out")
.GetScriptValue(),
V8ObjectBuilder(script_state)
.AddString("width", "0px")
.AddString("offset", "1")
.AddString("easing", "cubic-bezier(1, 1, 0.3, 0.3)")
.GetScriptValue()};
ScriptValue js_keyframes(
scope.GetIsolate(),
ToV8(blink_keyframes, scope.GetContext()->Global(), scope.GetIsolate()));
KeyframeEffect* animation =
CreateAnimationFromTiming(script_state, element.Get(), js_keyframes, 0);
Element* target = animation->target();
EXPECT_EQ(*element.Get(), *target);
const KeyframeVector keyframes = animation->Model()->GetFrames();
EXPECT_EQ(0, keyframes[0]->CheckedOffset());
EXPECT_EQ(1, keyframes[1]->CheckedOffset());
const CSSValue& keyframe1_width =
To<StringKeyframe>(*keyframes[0])
.CssPropertyValue(PropertyHandle(GetCSSPropertyWidth()));
const CSSValue& keyframe2_width =
To<StringKeyframe>(*keyframes[1])
.CssPropertyValue(PropertyHandle(GetCSSPropertyWidth()));
EXPECT_EQ("100px", keyframe1_width.CssText());
EXPECT_EQ("0px", keyframe2_width.CssText());
EXPECT_EQ(*(CubicBezierTimingFunction::Preset(
CubicBezierTimingFunction::EaseType::EASE_IN_OUT)),
keyframes[0]->Easing());
EXPECT_EQ(*(CubicBezierTimingFunction::Create(1, 1, 0.3, 0.3).get()),
keyframes[1]->Easing());
}
TEST_F(AnimationKeyframeEffectV8Test, SetAndRetrieveEffectComposite) {
V8TestingScope scope;
ScriptState* script_state = scope.GetScriptState();
NonThrowableExceptionState exception_state;
v8::Local<v8::Object> effect_options = v8::Object::New(scope.GetIsolate());
SetV8ObjectPropertyAsString(scope.GetIsolate(), effect_options, "composite",
"add");
KeyframeEffectOptions* effect_options_dictionary =
NativeValueTraits<KeyframeEffectOptions>::NativeValue(
scope.GetIsolate(), effect_options, exception_state);
EXPECT_FALSE(exception_state.HadException());
ScriptValue js_keyframes = ScriptValue::CreateNull(scope.GetIsolate());
KeyframeEffect* effect = CreateAnimationFromOption(
script_state, element.Get(), js_keyframes, effect_options_dictionary);
EXPECT_EQ("add", effect->composite());
effect->setComposite("replace");
EXPECT_EQ("replace", effect->composite());
effect->setComposite("accumulate");
EXPECT_EQ("accumulate", effect->composite());
}
TEST_F(AnimationKeyframeEffectV8Test, KeyframeCompositeOverridesEffect) {
V8TestingScope scope;
ScriptState* script_state = scope.GetScriptState();
NonThrowableExceptionState exception_state;
v8::Local<v8::Object> effect_options = v8::Object::New(scope.GetIsolate());
SetV8ObjectPropertyAsString(scope.GetIsolate(), effect_options, "composite",
"add");
KeyframeEffectOptions* effect_options_dictionary =
NativeValueTraits<KeyframeEffectOptions>::NativeValue(
scope.GetIsolate(), effect_options, exception_state);
EXPECT_FALSE(exception_state.HadException());
HeapVector<ScriptValue> blink_keyframes = {
V8ObjectBuilder(script_state)
.AddString("width", "100px")
.AddString("composite", "replace")
.GetScriptValue(),
V8ObjectBuilder(script_state).AddString("width", "0px").GetScriptValue()};
ScriptValue js_keyframes(
scope.GetIsolate(),
ToV8(blink_keyframes, scope.GetContext()->Global(), scope.GetIsolate()));
KeyframeEffect* effect = CreateAnimationFromOption(
script_state, element.Get(), js_keyframes, effect_options_dictionary);
EXPECT_EQ("add", effect->composite());
PropertyHandle property(GetCSSPropertyWidth());
const PropertySpecificKeyframeVector& keyframes =
*effect->Model()->GetPropertySpecificKeyframes(property);
EXPECT_EQ(EffectModel::kCompositeReplace, keyframes[0]->Composite());
EXPECT_EQ(EffectModel::kCompositeAdd, keyframes[1]->Composite());
}
TEST_F(AnimationKeyframeEffectV8Test, CanSetDuration) {
V8TestingScope scope;
ScriptState* script_state = scope.GetScriptState();
ScriptValue js_keyframes = ScriptValue::CreateNull(scope.GetIsolate());
double duration = 2000;
KeyframeEffect* animation = CreateAnimationFromTiming(
script_state, element.Get(), js_keyframes, duration);
EXPECT_TIMEDELTA(AnimationTimeDelta::FromMillisecondsD(duration),
animation->SpecifiedTiming().iteration_duration.value());
}
TEST_F(AnimationKeyframeEffectV8Test, CanOmitSpecifiedDuration) {
V8TestingScope scope;
ScriptState* script_state = scope.GetScriptState();
ScriptValue js_keyframes = ScriptValue::CreateNull(scope.GetIsolate());
KeyframeEffect* animation =
CreateAnimation(script_state, element.Get(), js_keyframes);
EXPECT_FALSE(animation->SpecifiedTiming().iteration_duration);
}
TEST_F(AnimationKeyframeEffectV8Test, SpecifiedGetters) {
V8TestingScope scope;
ScriptState* script_state = scope.GetScriptState();
ScriptValue js_keyframes = ScriptValue::CreateNull(scope.GetIsolate());
v8::Local<v8::Object> timing_input = v8::Object::New(scope.GetIsolate());
SetV8ObjectPropertyAsNumber(scope.GetIsolate(), timing_input, "delay", 2);
SetV8ObjectPropertyAsNumber(scope.GetIsolate(), timing_input, "endDelay",
0.5);
SetV8ObjectPropertyAsString(scope.GetIsolate(), timing_input, "fill",
"backwards");
SetV8ObjectPropertyAsNumber(scope.GetIsolate(), timing_input,
"iterationStart", 2);
SetV8ObjectPropertyAsNumber(scope.GetIsolate(), timing_input, "iterations",
10);
SetV8ObjectPropertyAsString(scope.GetIsolate(), timing_input, "direction",
"reverse");
SetV8ObjectPropertyAsString(scope.GetIsolate(), timing_input, "easing",
"ease-in-out");
DummyExceptionStateForTesting exception_state;
KeyframeEffectOptions* timing_input_dictionary =
NativeValueTraits<KeyframeEffectOptions>::NativeValue(
scope.GetIsolate(), timing_input, exception_state);
EXPECT_FALSE(exception_state.HadException());
KeyframeEffect* animation = CreateAnimationFromOption(
script_state, element.Get(), js_keyframes, timing_input_dictionary);
EffectTiming* timing = animation->getTiming();
EXPECT_EQ(2, timing->delay());
EXPECT_EQ(0.5, timing->endDelay());
EXPECT_EQ("backwards", timing->fill());
EXPECT_EQ(2, timing->iterationStart());
EXPECT_EQ(10, timing->iterations());
EXPECT_EQ("reverse", timing->direction());
EXPECT_EQ("ease-in-out", timing->easing());
}
TEST_F(AnimationKeyframeEffectV8Test, SpecifiedDurationGetter) {
V8TestingScope scope;
ScriptState* script_state = scope.GetScriptState();
ScriptValue js_keyframes = ScriptValue::CreateNull(scope.GetIsolate());
v8::Local<v8::Object> timing_input_with_duration =
v8::Object::New(scope.GetIsolate());
SetV8ObjectPropertyAsNumber(scope.GetIsolate(), timing_input_with_duration,
"duration", 2.5);
DummyExceptionStateForTesting exception_state;
KeyframeEffectOptions* timing_input_dictionary_with_duration =
NativeValueTraits<KeyframeEffectOptions>::NativeValue(
scope.GetIsolate(), timing_input_with_duration, exception_state);
EXPECT_FALSE(exception_state.HadException());
KeyframeEffect* animation_with_duration =
CreateAnimationFromOption(script_state, element.Get(), js_keyframes,
timing_input_dictionary_with_duration);
EffectTiming* specified_with_duration = animation_with_duration->getTiming();
UnrestrictedDoubleOrString duration = specified_with_duration->duration();
EXPECT_TRUE(duration.IsUnrestrictedDouble());
EXPECT_EQ(2.5, duration.GetAsUnrestrictedDouble());
EXPECT_FALSE(duration.IsString());
v8::Local<v8::Object> timing_input_no_duration =
v8::Object::New(scope.GetIsolate());
KeyframeEffectOptions* timing_input_dictionary_no_duration =
NativeValueTraits<KeyframeEffectOptions>::NativeValue(
scope.GetIsolate(), timing_input_no_duration, exception_state);
EXPECT_FALSE(exception_state.HadException());
KeyframeEffect* animation_no_duration =
CreateAnimationFromOption(script_state, element.Get(), js_keyframes,
timing_input_dictionary_no_duration);
EffectTiming* specified_no_duration = animation_no_duration->getTiming();
UnrestrictedDoubleOrString duration2 = specified_no_duration->duration();
EXPECT_FALSE(duration2.IsUnrestrictedDouble());
EXPECT_TRUE(duration2.IsString());
EXPECT_EQ("auto", duration2.GetAsString());
}
TEST_F(AnimationKeyframeEffectV8Test, SetKeyframesAdditiveCompositeOperation) {
// AnimationWorklet also needs to be disabled since it depends on
// WebAnimationsAPI and prevents us from turning it off if enabled.
ScopedAnimationWorkletForTest no_animation_worklet(false);
ScopedWebAnimationsAPIForTest no_web_animations(false);
V8TestingScope scope;
ScriptState* script_state = scope.GetScriptState();
ScriptValue js_keyframes = ScriptValue::CreateNull(scope.GetIsolate());
v8::Local<v8::Object> timing_input = v8::Object::New(scope.GetIsolate());
DummyExceptionStateForTesting exception_state;
KeyframeEffectOptions* timing_input_dictionary =
NativeValueTraits<KeyframeEffectOptions>::NativeValue(
scope.GetIsolate(), timing_input, exception_state);
ASSERT_FALSE(exception_state.HadException());
// Since there are no CSS-targeting keyframes, we can create a KeyframeEffect
// with composite = 'add'.
timing_input_dictionary->setComposite("add");
KeyframeEffect* effect = CreateAnimationFromOption(
script_state, element.Get(), js_keyframes, timing_input_dictionary);
EXPECT_EQ(effect->Model()->Composite(), EffectModel::kCompositeAdd);
// But if we then setKeyframes with CSS-targeting keyframes, the composite
// should fallback to 'replace'.
HeapVector<ScriptValue> blink_keyframes = {
V8ObjectBuilder(script_state).AddString("width", "10px").GetScriptValue(),
V8ObjectBuilder(script_state).AddString("width", "0px").GetScriptValue()};
ScriptValue new_js_keyframes(
scope.GetIsolate(),
ToV8(blink_keyframes, scope.GetContext()->Global(), scope.GetIsolate()));
effect->setKeyframes(script_state, new_js_keyframes, exception_state);
ASSERT_FALSE(exception_state.HadException());
EXPECT_EQ(effect->Model()->Composite(), EffectModel::kCompositeReplace);
}
TEST_F(KeyframeEffectTest, TimeToEffectChange) {
Timing timing;
timing.iteration_duration = AnimationTimeDelta::FromSecondsD(100);
timing.start_delay = 100;
timing.end_delay = 100;
timing.fill_mode = Timing::FillMode::NONE;
auto* keyframe_effect = MakeGarbageCollected<KeyframeEffect>(
nullptr, CreateEmptyEffectModel(), timing);
Animation* animation = GetDocument().Timeline().Play(keyframe_effect);
// Beginning of the animation.
EXPECT_TIMEDELTA(AnimationTimeDelta::FromSecondsD(100),
keyframe_effect->TimeToForwardsEffectChange());
EXPECT_EQ(AnimationTimeDelta::Max(),
keyframe_effect->TimeToReverseEffectChange());
// End of the before phase.
animation->setCurrentTime(CSSNumberish::FromDouble(100000));
EXPECT_TIMEDELTA(AnimationTimeDelta::FromSecondsD(100),
keyframe_effect->TimeToForwardsEffectChange());
EXPECT_TIMEDELTA(AnimationTimeDelta(),
keyframe_effect->TimeToReverseEffectChange());
// Nearing the end of the active phase.
animation->setCurrentTime(CSSNumberish::FromDouble(199000));
EXPECT_TIMEDELTA(AnimationTimeDelta::FromSecondsD(1),
keyframe_effect->TimeToForwardsEffectChange());
EXPECT_TIMEDELTA(AnimationTimeDelta(),
keyframe_effect->TimeToReverseEffectChange());
// End of the active phase.
animation->setCurrentTime(CSSNumberish::FromDouble(200000));
EXPECT_TIMEDELTA(AnimationTimeDelta::FromSecondsD(100),
keyframe_effect->TimeToForwardsEffectChange());
EXPECT_TIMEDELTA(AnimationTimeDelta(),
keyframe_effect->TimeToReverseEffectChange());
// End of the animation.
animation->setCurrentTime(CSSNumberish::FromDouble(300000));
EXPECT_EQ(AnimationTimeDelta::Max(),
keyframe_effect->TimeToForwardsEffectChange());
EXPECT_TIMEDELTA(AnimationTimeDelta::FromSecondsD(100),
keyframe_effect->TimeToReverseEffectChange());
}
TEST_F(KeyframeEffectTest, CheckCanStartAnimationOnCompositorNoKeyframes) {
ASSERT_TRUE(element);
const double animation_playback_rate = 1;
Timing timing;
// No keyframes results in an invalid animation.
{
auto* keyframe_effect = MakeGarbageCollected<KeyframeEffect>(
element, CreateEmptyEffectModel(), timing);
EXPECT_TRUE(keyframe_effect->CheckCanStartAnimationOnCompositor(
nullptr, animation_playback_rate) &
CompositorAnimations::kInvalidAnimationOrEffect);
}
// Keyframes but no properties results in an invalid animation.
{
StringKeyframeVector keyframes(2);
keyframes[0] = MakeGarbageCollected<StringKeyframe>();
keyframes[0]->SetOffset(0.0);
keyframes[1] = MakeGarbageCollected<StringKeyframe>();
keyframes[1]->SetOffset(1.0);
auto* effect_model =
MakeGarbageCollected<StringKeyframeEffectModel>(keyframes);
auto* keyframe_effect =
MakeGarbageCollected<KeyframeEffect>(element, effect_model, timing);
EXPECT_TRUE(keyframe_effect->CheckCanStartAnimationOnCompositor(
nullptr, animation_playback_rate) &
CompositorAnimations::kInvalidAnimationOrEffect);
}
}
TEST_F(KeyframeEffectTest, CheckCanStartAnimationOnCompositorNoTarget) {
const double animation_playback_rate = 1;
Timing timing;
// No target results in an invalid animation.
StringKeyframeVector keyframes(2);
keyframes[0] = MakeGarbageCollected<StringKeyframe>();
keyframes[0]->SetOffset(0.0);
keyframes[0]->SetCSSPropertyValue(CSSPropertyID::kLeft, "0px",
SecureContextMode::kInsecureContext,
nullptr);
keyframes[1] = MakeGarbageCollected<StringKeyframe>();
keyframes[1]->SetOffset(1.0);
keyframes[1]->SetCSSPropertyValue(CSSPropertyID::kLeft, "10px",
SecureContextMode::kInsecureContext,
nullptr);
auto* effect_model =
MakeGarbageCollected<StringKeyframeEffectModel>(keyframes);
auto* keyframe_effect =
MakeGarbageCollected<KeyframeEffect>(nullptr, effect_model, timing);
EXPECT_TRUE(keyframe_effect->CheckCanStartAnimationOnCompositor(
nullptr, animation_playback_rate) &
CompositorAnimations::kInvalidAnimationOrEffect);
}
TEST_F(KeyframeEffectTest, CheckCanStartAnimationOnCompositorBadTarget) {
const double animation_playback_rate = 1;
Timing timing;
StringKeyframeVector keyframes(2);
keyframes[0] = MakeGarbageCollected<StringKeyframe>();
keyframes[0]->SetOffset(0.0);
keyframes[0]->SetCSSPropertyValue(CSSPropertyID::kLeft, "0px",
SecureContextMode::kInsecureContext,
nullptr);
keyframes[1] = MakeGarbageCollected<StringKeyframe>();
keyframes[1]->SetOffset(1.0);
keyframes[1]->SetCSSPropertyValue(CSSPropertyID::kLeft, "10px",
SecureContextMode::kInsecureContext,
nullptr);
auto* effect_model =
MakeGarbageCollected<StringKeyframeEffectModel>(keyframes);
auto* keyframe_effect =
MakeGarbageCollected<KeyframeEffect>(element, effect_model, timing);
// If the target has a CSS offset we can't composite it.
element->SetInlineStyleProperty(CSSPropertyID::kOffsetPosition, "50px 50px");
UpdateAllLifecyclePhasesForTest();
ASSERT_TRUE(element->GetComputedStyle()->HasOffset());
EXPECT_TRUE(keyframe_effect->CheckCanStartAnimationOnCompositor(
nullptr, animation_playback_rate) &
CompositorAnimations::kTargetHasCSSOffset);
// If the target has multiple transform properties we can't composite it.
element->SetInlineStyleProperty(CSSPropertyID::kRotate, "90deg");
element->SetInlineStyleProperty(CSSPropertyID::kScale, "2 1");
UpdateAllLifecyclePhasesForTest();
EXPECT_TRUE(keyframe_effect->CheckCanStartAnimationOnCompositor(
nullptr, animation_playback_rate) &
CompositorAnimations::kTargetHasMultipleTransformProperties);
}
TEST_F(KeyframeEffectTest, TranslationTransformsPreserveAxisAlignment) {
auto* effect =
GetTwoFrameEffect(CSSPropertyID::kTransform, "translate(10px, 10px)",
"translate(20px, 20px)");
EXPECT_TRUE(effect->UpdateBoxSizeAndCheckTransformAxisAlignment(FloatSize()));
}
TEST_F(KeyframeEffectTest, ScaleTransformsPreserveAxisAlignment) {
auto* effect =
GetTwoFrameEffect(CSSPropertyID::kTransform, "scale(2)", "scale(3)");
EXPECT_TRUE(effect->UpdateBoxSizeAndCheckTransformAxisAlignment(FloatSize()));
}
TEST_F(KeyframeEffectTest, RotationTransformsDoNotPreserveAxisAlignment) {
auto* effect = GetTwoFrameEffect(CSSPropertyID::kTransform, "rotate(10deg)",
"rotate(20deg)");
EXPECT_FALSE(
effect->UpdateBoxSizeAndCheckTransformAxisAlignment(FloatSize()));
}
TEST_F(KeyframeEffectTest, RotationsDoNotPreserveAxisAlignment) {
auto* effect = GetTwoFrameEffect(CSSPropertyID::kRotate, "10deg", "20deg");
EXPECT_FALSE(
effect->UpdateBoxSizeAndCheckTransformAxisAlignment(FloatSize()));
}
} // namespace blink