Call SlideOutController.Delegate.OnSlideChanged at the right timing

Previously, OnSlideChanged was not called when the notification went
back to the origin position after the animation ends, but when the
animation starts. So we had the detection code in SlideHelper.Update()
and there was a bug of no control buttons (crr crbug.com/949436).

This CL refactors the CL and makes it called at the correct timing
(just after animation is finished) and the correct |in_progress|
status.

Also this CL adds some tests of SlideOutController.

Bug: 949436
Test: manual (did the steps on the bug, and observed as expected)

Change-Id: I0fdc7b6173ef2ad415ce981b482b65f319b948d8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1552209
Reviewed-by: Tim Song <tengs@chromium.org>
Commit-Queue: Yoshiki Iguchi <yoshiki@chromium.org>
Cr-Commit-Position: refs/heads/master@{#649021}
diff --git a/ash/system/message_center/arc/arc_notification_content_view.cc b/ash/system/message_center/arc/arc_notification_content_view.cc
index 18fc025..a927db2 100644
--- a/ash/system/message_center/arc/arc_notification_content_view.cc
+++ b/ash/system/message_center/arc/arc_notification_content_view.cc
@@ -199,35 +199,23 @@
   }
   virtual ~SlideHelper() = default;
 
-  void Update(base::Optional<bool> slide_in_progress) {
-    if (slide_in_progress.has_value())
-      slide_in_progress_ = slide_in_progress.value();
-
-    const bool has_animation =
-        GetSlideOutLayer()->GetAnimator()->is_animating();
-    const bool has_transform = !GetSlideOutLayer()->transform().IsIdentity();
-    const bool moving = (slide_in_progress_ && has_transform) || has_animation;
-
-    if (moving_ == moving)
+  void Update(bool slide_in_progress) {
+    if (slide_in_progress_ == slide_in_progress)
       return;
-    moving_ = moving;
 
-    if (moving_)
+    slide_in_progress_ = slide_in_progress;
+
+    if (slide_in_progress_)
       owner_->ShowCopiedSurface();
     else
       owner_->HideCopiedSurface();
   }
 
  private:
-  // This is a temporary hack to address https://crbug.com/718965
-  ui::Layer* GetSlideOutLayer() {
-    ui::Layer* layer = owner_->parent()->layer();
-    return layer ? layer : owner_->GetWidget()->GetLayer();
-  }
-
   ArcNotificationContentView* const owner_;
+
+  // True if the view is not at the original position.
   bool slide_in_progress_ = false;
-  bool moving_ = false;
 
   DISALLOW_COPY_AND_ASSIGN(SlideHelper);
 };
@@ -358,6 +346,7 @@
 }
 
 void ArcNotificationContentView::OnSlideChanged(bool in_progress) {
+  slide_in_progress_ = in_progress;
   if (slide_helper_)
     slide_helper_->Update(in_progress);
 }
@@ -497,7 +486,7 @@
   slide_helper_.reset(new SlideHelper(this));
 
   // Invokes Update() in case surface is attached during a slide.
-  slide_helper_->Update(base::nullopt);
+  slide_helper_->Update(slide_in_progress_);
 
   // (Re-)create the floating buttons after |surface_| is attached to a widget.
   MaybeCreateFloatingControlButtons();
diff --git a/ash/system/message_center/arc/arc_notification_content_view.h b/ash/system/message_center/arc/arc_notification_content_view.h
index 2ac137d7..d835b3e 100644
--- a/ash/system/message_center/arc/arc_notification_content_view.h
+++ b/ash/system/message_center/arc/arc_notification_content_view.h
@@ -154,6 +154,19 @@
   // when a slide is in progress and restore the surface when it finishes.
   std::unique_ptr<SlideHelper> slide_helper_;
 
+  // Whether the notification is being slid or is at the origin. This stores the
+  // latest value of the |in_progress| from OnSlideChanged callback, which is
+  // called during both manual swipe and automatic slide on dismissing or
+  // resetting back to the origin.
+  // This value is synced with the visibility of the copied surface. If the
+  // value is true, the copied surface is visible instead of the original
+  // surface itself. Copied surgace doesn't have control buttons so they must be
+  // hidden if it's true.
+  // This value is stored in case of the change of surface. When a new surface
+  // sets, if this value is true, the copy of the new surface gets visible
+  // instead of the copied surface itself.
+  bool slide_in_progress_ = false;
+
   // A control buttons on top of NotificationSurface. Needed because the
   // aura::Window of NotificationSurface is added after hosting widget's
   // RootView thus standard notification control buttons are always below
diff --git a/ui/message_center/views/slide_out_controller.cc b/ui/message_center/views/slide_out_controller.cc
index b49a703..d9d975b1 100644
--- a/ui/message_center/views/slide_out_controller.cc
+++ b/ui/message_center/views/slide_out_controller.cc
@@ -12,6 +12,11 @@
 
 namespace message_center {
 
+namespace {
+constexpr int kSwipeRestoreDurationMs = 150;
+constexpr int kSwipeOutTotalDurationMs = 150;
+}  // anonymous namespace
+
 SlideOutController::SlideOutController(ui::EventTarget* target,
                                        Delegate* delegate)
     : target_handling_(target, this), delegate_(delegate) {}
@@ -45,13 +50,11 @@
     if (mode_ == SlideMode::FULL &&
         fabsf(event->details().velocity_x()) > kFlingThresholdForClose) {
       SlideOutAndClose(event->details().velocity_x());
-      delegate_->OnSlideChanged(false);
       event->StopPropagation();
       return;
     }
     CaptureControlOpenState();
     RestoreVisualState();
-    delegate_->OnSlideChanged(false);
     return;
   }
 
@@ -111,26 +114,18 @@
     if (mode_ == SlideMode::FULL &&
         scrolled_ratio >= scroll_amount_for_closing_notification / width) {
       SlideOutAndClose(gesture_amount_);
-      delegate_->OnSlideChanged(false);
       event->StopPropagation();
       return;
     }
     CaptureControlOpenState();
     RestoreVisualState();
-    delegate_->OnSlideChanged(false);
   }
 
   event->SetHandled();
 }
 
 void SlideOutController::RestoreVisualState() {
-  ui::Layer* layer = delegate_->GetSlideOutLayer();
   // Restore the layer state.
-  const int kSwipeRestoreDurationMS = 150;
-  ui::ScopedLayerAnimationSettings settings(layer->GetAnimator());
-  settings.SetTransitionDuration(
-      base::TimeDelta::FromMilliseconds(kSwipeRestoreDurationMS));
-  settings.AddObserver(this);
   gfx::Transform transform;
   switch (control_open_state_) {
     case SwipeControlOpenState::CLOSED:
@@ -144,36 +139,21 @@
       break;
   }
 
-  if (layer->transform() == transform && opacity_ == 1.f) {
-    // Nothing are changed and no animation starts.
-    return;
-  }
-
-  // In this case, animation starts. OnImplicitAnimationsCompleted will be
-  // called just after the animation finishes.
-  layer->SetTransform(transform);
   SetOpacityIfNecessary(1.f);
-  delegate_->OnSlideChanged(true);
+  SetTransformWithAnimationIfNecessary(
+      transform, base::TimeDelta::FromMilliseconds(kSwipeRestoreDurationMs));
 }
 
 void SlideOutController::SlideOutAndClose(int direction) {
   ui::Layer* layer = delegate_->GetSlideOutLayer();
-  const int kSwipeOutTotalDurationMS = 150;
-  int swipe_out_duration = kSwipeOutTotalDurationMS * opacity_;
-  ui::ScopedLayerAnimationSettings settings(layer->GetAnimator());
-  settings.SetTransitionDuration(
-      base::TimeDelta::FromMilliseconds(swipe_out_duration));
-  settings.AddObserver(this);
-
   gfx::Transform transform;
   int width = layer->bounds().width();
   transform.Translate(direction < 0 ? -width : width, 0.0);
 
-  // An animation starts. OnImplicitAnimationsCompleted will be called just
-  // after the animation finishes.
-  layer->SetTransform(transform);
+  int swipe_out_duration = kSwipeOutTotalDurationMs * opacity_;
   SetOpacityIfNecessary(0.f);
-  delegate_->OnSlideChanged(true);
+  SetTransformWithAnimationIfNecessary(
+      transform, base::TimeDelta::FromMilliseconds(swipe_out_duration));
 }
 
 void SlideOutController::SetOpacityIfNecessary(float opacity) {
@@ -182,8 +162,47 @@
   opacity_ = opacity;
 }
 
+void SlideOutController::SetTransformWithAnimationIfNecessary(
+    const gfx::Transform& transform,
+    base::TimeDelta animation_duration) {
+  ui::Layer* layer = delegate_->GetSlideOutLayer();
+  if (layer->transform() != transform) {
+    ui::ScopedLayerAnimationSettings settings(layer->GetAnimator());
+    settings.SetTransitionDuration(animation_duration);
+    settings.AddObserver(this);
+
+    // An animation starts. OnImplicitAnimationsCompleted will be called just
+    // after the animation finishes.
+    layer->SetTransform(transform);
+
+    // Notify slide changed with inprogress=true, since the element will slide
+    // with animation. OnSlideChanged(false) will be called after animation.
+    delegate_->OnSlideChanged(true);
+  } else {
+    // Notify slide changed after the animation finishes.
+    // The argument in_progress is true if the target view is back at the
+    // origin or has been gone. False if the target is visible but not at
+    // the origin. False if the target is visible but not at
+    // the origin.
+    const bool in_progress = !layer->transform().IsIdentity();
+    delegate_->OnSlideChanged(in_progress);
+  }
+}
+
 void SlideOutController::OnImplicitAnimationsCompleted() {
-  if (opacity_ > 0)
+  // Here the situation is either of:
+  // 1) Notification is slided out and is about to be removed
+  //      => |in_progress| is false, calling OnSlideOut
+  // 2) Notification is at the origin => |in_progress| is false
+  // 3) Notification is snapped to the swipe control => |in_progress| is true
+
+  const bool is_completely_slid_out = (opacity_ == 0);
+  const bool in_progress =
+      !delegate_->GetSlideOutLayer()->transform().IsIdentity() &&
+      !is_completely_slid_out;
+  delegate_->OnSlideChanged(in_progress);
+
+  if (!is_completely_slid_out)
     return;
 
   // Call Delegate::OnSlideOut() if this animation came from SlideOutAndClose().
diff --git a/ui/message_center/views/slide_out_controller.h b/ui/message_center/views/slide_out_controller.h
index 1a1ec5c..ed10291 100644
--- a/ui/message_center/views/slide_out_controller.h
+++ b/ui/message_center/views/slide_out_controller.h
@@ -87,6 +87,10 @@
   // Sets the opacity of the slide out layer if |update_opacity_| is true.
   void SetOpacityIfNecessary(float opacity);
 
+  // Sets the transform matrix and performs animation if the matrix is changed.
+  void SetTransformWithAnimationIfNecessary(const gfx::Transform& transform,
+                                            base::TimeDelta animation_duration);
+
   ui::ScopedTargetHandler target_handling_;
   Delegate* delegate_;
 
diff --git a/ui/message_center/views/slide_out_controller_unittest.cc b/ui/message_center/views/slide_out_controller_unittest.cc
index ae2348f..ebd67735 100644
--- a/ui/message_center/views/slide_out_controller_unittest.cc
+++ b/ui/message_center/views/slide_out_controller_unittest.cc
@@ -9,6 +9,11 @@
 
 namespace message_center {
 
+namespace {
+constexpr int kSwipeControlWidth = 30;  // px
+constexpr int kTargetWidth = 200;       // px
+}  // namespace
+
 class SlideOutControllerDelegate : public SlideOutController::Delegate {
  public:
   explicit SlideOutControllerDelegate(views::View* target) : target_(target) {}
@@ -23,8 +28,17 @@
     ++slide_changed_count_;
   }
 
+  bool IsOnSlideChangedCalled() const { return (slide_changed_count_ > 0); }
+
   void OnSlideOut() override { ++slide_out_count_; }
 
+  void reset() {
+    slide_started_count_ = 0;
+    slide_changed_count_ = 0;
+    slide_out_count_ = 0;
+    slide_changed_last_value_ = base::nullopt;
+  }
+
   base::Optional<bool> slide_changed_last_value_;
   int slide_started_count_ = 0;
   int slide_changed_count_ = 0;
@@ -53,7 +67,7 @@
 
     views::View* target_ = new views::View();
     target_->SetPaintToLayer(ui::LAYER_TEXTURED);
-    target_->SetSize(gfx::Size(50, 50));
+    target_->SetSize(gfx::Size(kTargetWidth, 50));
 
     root->AddChildView(target_);
     widget_->Show();
@@ -78,44 +92,419 @@
 
   SlideOutControllerDelegate* delegate() { return delegate_.get(); }
 
+  void PostSequentialGestureEvent(const ui::GestureEventDetails& details) {
+    // Set the timestamp ahead one microsecond.
+    sequential_event_timestamp_ += base::TimeDelta::FromMicroseconds(1);
+
+    ui::GestureEvent gesture_event(
+        0, 0, ui::EF_NONE, base::TimeTicks() + sequential_event_timestamp_,
+        details);
+    slide_out_controller()->OnGestureEvent(&gesture_event);
+  }
+
+  void PostSequentialSwipeEvent(int swipe_amount) {
+    PostSequentialGestureEvent(
+        ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_BEGIN));
+    PostSequentialGestureEvent(
+        ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_UPDATE, swipe_amount, 0));
+    PostSequentialGestureEvent(
+        ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_END));
+  }
+
  private:
   std::unique_ptr<views::Widget> widget_;
   std::unique_ptr<SlideOutController> slide_out_controller_;
   std::unique_ptr<SlideOutControllerDelegate> delegate_;
+  base::TimeDelta sequential_event_timestamp_;
 };
 
 TEST_F(SlideOutControllerTest, OnGestureEventAndDelegate) {
-  ui::GestureEvent scroll_begin(
-      0, 0, ui::EF_NONE,
-      base::TimeTicks() + base::TimeDelta::FromMicroseconds(1),
+  PostSequentialGestureEvent(
       ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_BEGIN));
-  slide_out_controller()->OnGestureEvent(&scroll_begin);
+
+  EXPECT_EQ(1, delegate()->slide_started_count_);
+  EXPECT_FALSE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_EQ(0, delegate()->slide_out_count_);
+
+  delegate()->reset();
+
+  PostSequentialGestureEvent(
+      ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_UPDATE));
+
+  EXPECT_EQ(0, delegate()->slide_started_count_);
+  EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_TRUE(delegate()->slide_changed_last_value_.value());
+  EXPECT_EQ(0, delegate()->slide_out_count_);
+
+  delegate()->reset();
+
+  PostSequentialGestureEvent(
+      ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_END));
+
+  EXPECT_EQ(0, delegate()->slide_started_count_);
+  EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_FALSE(delegate()->slide_changed_last_value_.value());
+  EXPECT_EQ(0, delegate()->slide_out_count_);
+}
+
+TEST_F(SlideOutControllerTest, SlideOutAndClose) {
+  // Place a finger on notification.
+  PostSequentialGestureEvent(
+      ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_BEGIN));
 
   EXPECT_EQ(1, delegate()->slide_started_count_);
   EXPECT_EQ(0, delegate()->slide_changed_count_);
   EXPECT_EQ(0, delegate()->slide_out_count_);
 
-  ui::GestureEvent scroll_update(
-      0, 0, ui::EF_NONE,
-      base::TimeTicks() + base::TimeDelta::FromMicroseconds(2),
-      ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_UPDATE));
-  slide_out_controller()->OnGestureEvent(&scroll_update);
+  delegate()->reset();
 
-  EXPECT_EQ(1, delegate()->slide_started_count_);
-  EXPECT_EQ(1, delegate()->slide_changed_count_);
+  // Move the finger horizontally by 101 px. (101 px is more than half of the
+  // target width 200 px)
+  PostSequentialGestureEvent(ui::GestureEventDetails(
+      ui::ET_GESTURE_SCROLL_UPDATE, kTargetWidth / 2 + 1, 0));
+
+  EXPECT_EQ(0, delegate()->slide_started_count_);
+  EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
   EXPECT_TRUE(delegate()->slide_changed_last_value_.value());
   EXPECT_EQ(0, delegate()->slide_out_count_);
 
-  ui::GestureEvent scroll_end(
-      0, 0, ui::EF_NONE,
-      base::TimeTicks() + base::TimeDelta::FromMicroseconds(3),
+  delegate()->reset();
+
+  // Release the finger.
+  PostSequentialGestureEvent(
       ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_END));
-  slide_out_controller()->OnGestureEvent(&scroll_end);
+
+  EXPECT_EQ(0, delegate()->slide_started_count_);
+  EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_FALSE(delegate()->slide_changed_last_value_.value());
+  EXPECT_EQ(0, delegate()->slide_out_count_);
+  // The target has been scrolled out and the current location is moved by the
+  // width (200px).
+  EXPECT_EQ(kTargetWidth,
+            delegate()->GetSlideOutLayer()->transform().To2dTranslation().x());
+
+  delegate()->reset();
+
+  // Ensure a deferred OnSlideOut handler is called.
+  base::RunLoop().RunUntilIdle();
+  EXPECT_FALSE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_EQ(1, delegate()->slide_out_count_);
+}
+
+TEST_F(SlideOutControllerTest, SlideLittleAmountAndNotClose) {
+  // Place a finger on notification.
+  PostSequentialGestureEvent(
+      ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_BEGIN));
 
   EXPECT_EQ(1, delegate()->slide_started_count_);
-  EXPECT_EQ(2, delegate()->slide_changed_count_);
+  EXPECT_FALSE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_EQ(0, delegate()->slide_out_count_);
+
+  delegate()->reset();
+
+  // Move the finger horizontally by 99 px. (99 px is less than half of the
+  // target width 200 px)
+  PostSequentialGestureEvent(
+      ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_UPDATE, 99, 0));
+
+  EXPECT_EQ(0, delegate()->slide_started_count_);
+  EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_TRUE(delegate()->slide_changed_last_value_.value());
+  EXPECT_EQ(0, delegate()->slide_out_count_);
+
+  delegate()->reset();
+
+  // Release the finger.
+  PostSequentialGestureEvent(
+      ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_END));
+
+  EXPECT_EQ(0, delegate()->slide_started_count_);
+  EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
   EXPECT_FALSE(delegate()->slide_changed_last_value_.value());
   EXPECT_EQ(0, delegate()->slide_out_count_);
+  // The target has been moved back to the origin.
+  EXPECT_EQ(0.f,
+            delegate()->GetSlideOutLayer()->transform().To2dTranslation().x());
+
+  delegate()->reset();
+
+  // Ensure no deferred SlideOut handler.
+  base::RunLoop().RunUntilIdle();
+  EXPECT_FALSE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_EQ(0, delegate()->slide_out_count_);
+}
+
+TEST_F(SlideOutControllerTest, SetSwipeControlWidth_SwipeLessThanControlWidth) {
+  // Set the width of swipe control.
+  slide_out_controller()->SetSwipeControlWidth(kSwipeControlWidth);
+
+  // Place a finger on notification.
+  PostSequentialGestureEvent(
+      ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_BEGIN));
+
+  EXPECT_EQ(1, delegate()->slide_started_count_);
+  EXPECT_FALSE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_EQ(0, delegate()->slide_out_count_);
+
+  delegate()->reset();
+
+  // Move the finger horizontally by 29 px. (29 px is less than the swipe
+  // control width).
+  PostSequentialGestureEvent(ui::GestureEventDetails(
+      ui::ET_GESTURE_SCROLL_UPDATE, kSwipeControlWidth - 1, 0));
+
+  EXPECT_EQ(0, delegate()->slide_started_count_);
+  EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_TRUE(delegate()->slide_changed_last_value_.value());
+  EXPECT_EQ(0, delegate()->slide_out_count_);
+
+  delegate()->reset();
+
+  // Release the finger.
+  PostSequentialGestureEvent(
+      ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_END));
+
+  EXPECT_EQ(0, delegate()->slide_started_count_);
+  EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_FALSE(delegate()->slide_changed_last_value_.value());
+  EXPECT_EQ(0, delegate()->slide_out_count_);
+  // The target has been moved back to the origin.
+  EXPECT_EQ(0.f,
+            delegate()->GetSlideOutLayer()->transform().To2dTranslation().x());
+
+  delegate()->reset();
+
+  // Ensure no deferred SlideOut handler.
+  base::RunLoop().RunUntilIdle();
+  EXPECT_FALSE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_EQ(0, delegate()->slide_out_count_);
+}
+
+TEST_F(SlideOutControllerTest, SwipeControlWidth_SwipeMoreThanControlWidth) {
+  // Set the width of swipe control.
+  slide_out_controller()->SetSwipeControlWidth(kSwipeControlWidth);
+
+  // Place a finger on notification.
+  PostSequentialGestureEvent(
+      ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_BEGIN));
+
+  EXPECT_EQ(1, delegate()->slide_started_count_);
+  EXPECT_FALSE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_EQ(0, delegate()->slide_out_count_);
+
+  delegate()->reset();
+
+  // Move the finger horizontally by 31 px. (31 px is more than the swipe
+  // control width).
+  PostSequentialGestureEvent(ui::GestureEventDetails(
+      ui::ET_GESTURE_SCROLL_UPDATE, kSwipeControlWidth + 1, 0));
+
+  EXPECT_EQ(0, delegate()->slide_started_count_);
+  EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_TRUE(delegate()->slide_changed_last_value_.value());
+  EXPECT_EQ(0, delegate()->slide_out_count_);
+
+  delegate()->reset();
+
+  // Release the finger.
+  PostSequentialGestureEvent(
+      ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_END));
+
+  EXPECT_EQ(0, delegate()->slide_started_count_);
+  // Slide is in progress.
+  EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_TRUE(delegate()->slide_changed_last_value_.value());
+  EXPECT_EQ(0, delegate()->slide_out_count_);
+  // Swipe amount is the swipe control width.
+  EXPECT_EQ(kSwipeControlWidth,
+            delegate()->GetSlideOutLayer()->transform().To2dTranslation().x());
+
+  delegate()->reset();
+
+  // Ensure no deferred SlideOut handler.
+  base::RunLoop().RunUntilIdle();
+  EXPECT_FALSE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_EQ(0, delegate()->slide_out_count_);
+}
+
+TEST_F(SlideOutControllerTest, SetSwipeControlWidth_SwipeOut) {
+  // Set the width of swipe control.
+  slide_out_controller()->SetSwipeControlWidth(kSwipeControlWidth);
+
+  // Place a finger on notification.
+  PostSequentialGestureEvent(
+      ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_BEGIN));
+
+  EXPECT_EQ(1, delegate()->slide_started_count_);
+  EXPECT_FALSE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_EQ(0, delegate()->slide_out_count_);
+
+  delegate()->reset();
+
+  // Move the finger horizontally by 101 px. (101 px is more than the half of
+  // the target width).
+  PostSequentialGestureEvent(ui::GestureEventDetails(
+      ui::ET_GESTURE_SCROLL_UPDATE, kTargetWidth / 2 + 1, 0));
+
+  EXPECT_EQ(0, delegate()->slide_started_count_);
+  EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_TRUE(delegate()->slide_changed_last_value_.value());
+  EXPECT_EQ(0, delegate()->slide_out_count_);
+
+  delegate()->reset();
+
+  // Release the finger.
+  PostSequentialGestureEvent(
+      ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_END));
+
+  // ... and it is automatically slided out.
+  EXPECT_EQ(0, delegate()->slide_started_count_);
+  EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_FALSE(delegate()->slide_changed_last_value_.value());
+  EXPECT_EQ(0, delegate()->slide_out_count_);
+  EXPECT_EQ(kTargetWidth,
+            delegate()->GetSlideOutLayer()->transform().To2dTranslation().x());
+
+  delegate()->reset();
+
+  // Ensure a deferred SlideOut handler is called once.
+  base::RunLoop().RunUntilIdle();
+  EXPECT_FALSE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_EQ(1, delegate()->slide_out_count_);
+}
+
+TEST_F(SlideOutControllerTest, SwipeControlWidth_SnapAndSwipeOut) {
+  // Set the width of swipe control.
+  slide_out_controller()->SetSwipeControlWidth(kSwipeControlWidth);
+
+  // Snap to the swipe control.
+  PostSequentialGestureEvent(
+      ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_BEGIN));
+  PostSequentialGestureEvent(ui::GestureEventDetails(
+      ui::ET_GESTURE_SCROLL_UPDATE, kSwipeControlWidth, 0));
+  PostSequentialGestureEvent(
+      ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_END));
+  EXPECT_EQ(1, delegate()->slide_started_count_);
+  EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_TRUE(delegate()->slide_changed_last_value_.value());
+  EXPECT_EQ(0, delegate()->slide_out_count_);
+  EXPECT_EQ(kSwipeControlWidth,
+            delegate()->GetSlideOutLayer()->transform().To2dTranslation().x());
+
+  delegate()->reset();
+
+  // Swipe horizontally by 70 px.
+  PostSequentialGestureEvent(
+      ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_BEGIN));
+  PostSequentialGestureEvent(
+      ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_UPDATE, 70, 0));
+  PostSequentialGestureEvent(
+      ui::GestureEventDetails(ui::ET_GESTURE_SCROLL_END));
+
+  // ... and it is automatically slided out.
+  EXPECT_EQ(1, delegate()->slide_started_count_);
+  EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_FALSE(delegate()->slide_changed_last_value_.value());
+  EXPECT_EQ(0, delegate()->slide_out_count_);
+  EXPECT_EQ(kTargetWidth,
+            delegate()->GetSlideOutLayer()->transform().To2dTranslation().x());
+
+  delegate()->reset();
+
+  // Ensure a deferred OnSlideOut handler is called.
+  base::RunLoop().RunUntilIdle();
+  EXPECT_FALSE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_EQ(1, delegate()->slide_out_count_);
+}
+
+TEST_F(SlideOutControllerTest, SwipeControlWidth_SnapAndSnapToControl) {
+  // Set the width of swipe control.
+  slide_out_controller()->SetSwipeControlWidth(kSwipeControlWidth);
+
+  // Snap to the swipe control.
+  PostSequentialSwipeEvent(kSwipeControlWidth + 10);
+  EXPECT_EQ(1, delegate()->slide_started_count_);
+  EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_TRUE(delegate()->slide_changed_last_value_.value());
+  EXPECT_EQ(0, delegate()->slide_out_count_);
+  EXPECT_EQ(kSwipeControlWidth,
+            delegate()->GetSlideOutLayer()->transform().To2dTranslation().x());
+
+  delegate()->reset();
+
+  // Swipe horizontally by 40 px for the same direction.
+  PostSequentialSwipeEvent(40);
+
+  // Snap automatically back to the swipe control.
+  EXPECT_EQ(1, delegate()->slide_started_count_);
+  EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_TRUE(delegate()->slide_changed_last_value_.value());
+  EXPECT_EQ(0, delegate()->slide_out_count_);
+  EXPECT_EQ(kSwipeControlWidth,
+            delegate()->GetSlideOutLayer()->transform().To2dTranslation().x());
+
+  delegate()->reset();
+
+  // Ensure no deferred OnSlideOut handler.
+  base::RunLoop().RunUntilIdle();
+  EXPECT_FALSE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_EQ(0, delegate()->slide_out_count_);
+}
+
+TEST_F(SlideOutControllerTest, SwipeControlWidth_SnapAndBackToOrigin) {
+  // Set the width of swipe control.
+  slide_out_controller()->SetSwipeControlWidth(kSwipeControlWidth);
+
+  // Snap to the swipe control.
+  PostSequentialSwipeEvent(kSwipeControlWidth + 20);
+  EXPECT_EQ(1, delegate()->slide_started_count_);
+  EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_TRUE(delegate()->slide_changed_last_value_.value());
+  EXPECT_EQ(0, delegate()->slide_out_count_);
+  EXPECT_EQ(kSwipeControlWidth,
+            delegate()->GetSlideOutLayer()->transform().To2dTranslation().x());
+
+  delegate()->reset();
+
+  // Swipe to the reversed direction by -1 px.
+  PostSequentialSwipeEvent(-1);
+
+  // Snap automatically back to the origin.
+  EXPECT_EQ(1, delegate()->slide_started_count_);
+  EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_FALSE(delegate()->slide_changed_last_value_.value());
+  EXPECT_EQ(0, delegate()->slide_out_count_);
+  EXPECT_EQ(0,
+            delegate()->GetSlideOutLayer()->transform().To2dTranslation().x());
+
+  delegate()->reset();
+
+  // Ensure no deferred OnSlideOut handler.
+  base::RunLoop().RunUntilIdle();
+  EXPECT_FALSE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_EQ(0, delegate()->slide_out_count_);
+}
+
+TEST_F(SlideOutControllerTest, SwipeControlWidth_NotSnapAndBackToOrigin) {
+  // Set the width of swipe control.
+  slide_out_controller()->SetSwipeControlWidth(kSwipeControlWidth);
+
+  // Swipe partially but it's not enough to snap to the swipe control. So it is
+  // back to the origin
+  PostSequentialSwipeEvent(kSwipeControlWidth - 1);
+  EXPECT_EQ(1, delegate()->slide_started_count_);
+  EXPECT_TRUE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_FALSE(delegate()->slide_changed_last_value_.value());
+  EXPECT_EQ(0, delegate()->slide_out_count_);
+  EXPECT_EQ(0,
+            delegate()->GetSlideOutLayer()->transform().To2dTranslation().x());
+
+  delegate()->reset();
+
+  // Ensure no deferred OnSlideOut handler.
+  base::RunLoop().RunUntilIdle();
+  EXPECT_FALSE(delegate()->IsOnSlideChangedCalled());
+  EXPECT_EQ(0, delegate()->slide_out_count_);
 }
 
 }  // namespace message_center