[iOS] Improve FullscreenModel scroll handling.

This CL improves fullscreen handling for several edge
conditions.  The behavior described in the referenced bug
is due to FullscreenModel attempting to handle UIScrollView
adjustments that occur when bouncing past the top edge of
content.  Similarly broken behavior also occurred when
attempting to scroll pages smaller or similarly sized with
the scroll view.

After this change, FullscreenModel ignores scrolls that
have to do with UIScrollView adjusting its contentOffset
for bouncing or resizing behavior.

Implementing this logic also required exposing the
scroll view frame, contentInset, contentSize, and zooming
properties.

TBR=kkhorimoto@chromium.org

(cherry picked from commit 963abca215a516aab5fbe81e17fb96466d327222)

Bug: 800757, 807957, 809853, 809856
Cq-Include-Trybots: master.tryserver.chromium.mac:ios-simulator-cronet;master.tryserver.chromium.mac:ios-simulator-full-configs
Change-Id: I8980983e048ce73d3443160cb4dd2e29a1aed15a
Reviewed-on: https://chromium-review.googlesource.com/896304
Commit-Queue: Kurt Horimoto <kkhorimoto@chromium.org>
Reviewed-by: Mark Cogan <marq@chromium.org>
Reviewed-by: Justin Cohen <justincohen@chromium.org>
Reviewed-by: Kurt Horimoto <kkhorimoto@chromium.org>
Reviewed-by: Sergio Collazos <sczs@chromium.org>
Cr-Original-Commit-Position: refs/heads/master@{#535435}
Reviewed-on: https://chromium-review.googlesource.com/932548
Cr-Commit-Position: refs/branch-heads/3325@{#555}
Cr-Branched-From: bc084a8b5afa3744a74927344e304c02ae54189f-refs/heads/master@{#530369}
diff --git a/ios/chrome/browser/ui/broadcaster/chrome_broadcast_observer.h b/ios/chrome/browser/ui/broadcaster/chrome_broadcast_observer.h
index 58500b8..8aa965dd 100644
--- a/ios/chrome/browser/ui/broadcaster/chrome_broadcast_observer.h
+++ b/ios/chrome/browser/ui/broadcaster/chrome_broadcast_observer.h
@@ -18,6 +18,18 @@
 
 #pragma mark - Scrolling events
 
+// Observer method for objects that care about the size of the scroll view
+// displaying the main content.
+- (void)broadcastScrollViewSize:(CGSize)scrollViewSize;
+
+// Observer method for objects that care about the height of the current page's
+// rendered contents.
+- (void)broadcastScrollViewContentSize:(CGSize)contentSize;
+
+// Observer method for objects that care about the content inset for the scroll
+// view displaying the main content area.
+- (void)broadcastScrollViewContentInset:(UIEdgeInsets)contentInset;
+
 // Observer method for objects that care about the current vertical (y-axis)
 // scroll offset of the tab content area.
 - (void)broadcastContentScrollOffset:(CGFloat)offset;
@@ -27,6 +39,10 @@
 - (void)broadcastScrollViewIsScrolling:(BOOL)scrolling;
 
 // Observer method for objects that care about whether the main content area is
+// zooming.
+- (void)broadcastScrollViewIsZooming:(BOOL)zooming;
+
+// Observer method for objects that care about whether the main content area is
 // being dragged.  Note that if a drag ends with residual velocity, it's
 // possible for |dragging| to be NO while |scrolling| is still YES.
 - (void)broadcastScrollViewIsDragging:(BOOL)dragging;
diff --git a/ios/chrome/browser/ui/broadcaster/chrome_broadcast_observer_bridge.h b/ios/chrome/browser/ui/broadcaster/chrome_broadcast_observer_bridge.h
index 5db36bd..fa7b64c 100644
--- a/ios/chrome/browser/ui/broadcaster/chrome_broadcast_observer_bridge.h
+++ b/ios/chrome/browser/ui/broadcaster/chrome_broadcast_observer_bridge.h
@@ -12,12 +12,24 @@
  public:
   virtual ~ChromeBroadcastObserverInterface();
 
+  // Invoked by |-broadcastScrollViewSize:|.
+  virtual void OnScrollViewSizeBroadcasted(CGSize scroll_view_size) {}
+
+  // Invoked by |-broadcastScrollViewContentSize:|.
+  virtual void OnScrollViewContentSizeBroadcasted(CGSize content_size) {}
+
+  // Invoked by |-broadcastScrollViewContentInset:|.
+  virtual void OnScrollViewContentInsetBroadcasted(UIEdgeInsets conent_inset) {}
+
   // Invoked by |-broadcastContentScrollOffset:|.
   virtual void OnContentScrollOffsetBroadcasted(CGFloat offset) {}
 
   // Invoked by |-broadcastScrollViewIsScrolling:|.
   virtual void OnScrollViewIsScrollingBroadcasted(bool scrolling) {}
 
+  // Invoked by |-broadcastScrollViewIsZooming:|.
+  virtual void OnScrollViewIsZoomingBroadcasted(bool zooming) {}
+
   // Invoked by |-broadcastScrollViewIsDragging:|.
   virtual void OnScrollViewIsDraggingBroadcasted(bool dragging) {}
 
diff --git a/ios/chrome/browser/ui/broadcaster/chrome_broadcast_observer_bridge.mm b/ios/chrome/browser/ui/broadcaster/chrome_broadcast_observer_bridge.mm
index 6c4e3da..7eede52 100644
--- a/ios/chrome/browser/ui/broadcaster/chrome_broadcast_observer_bridge.mm
+++ b/ios/chrome/browser/ui/broadcaster/chrome_broadcast_observer_bridge.mm
@@ -23,6 +23,18 @@
   return self;
 }
 
+- (void)broadcastScrollViewSize:(CGSize)scrollViewSize {
+  self.observer->OnScrollViewSizeBroadcasted(scrollViewSize);
+}
+
+- (void)broadcastScrollViewContentSize:(CGSize)contentSize {
+  self.observer->OnScrollViewContentSizeBroadcasted(contentSize);
+}
+
+- (void)broadcastScrollViewContentInset:(UIEdgeInsets)contentInset {
+  self.observer->OnScrollViewContentInsetBroadcasted(contentInset);
+}
+
 - (void)broadcastContentScrollOffset:(CGFloat)offset {
   self.observer->OnContentScrollOffsetBroadcasted(offset);
 }
@@ -31,6 +43,10 @@
   self.observer->OnScrollViewIsScrollingBroadcasted(scrolling);
 }
 
+- (void)broadcastScrollViewIsZooming:(BOOL)zooming {
+  self.observer->OnScrollViewIsZoomingBroadcasted(zooming);
+}
+
 - (void)broadcastScrollViewIsDragging:(BOOL)dragging {
   self.observer->OnScrollViewIsDraggingBroadcasted(dragging);
 }
diff --git a/ios/chrome/browser/ui/broadcaster/chrome_broadcaster.mm b/ios/chrome/browser/ui/broadcaster/chrome_broadcaster.mm
index 4b3606b..c4f077c 100644
--- a/ios/chrome/browser/ui/broadcaster/chrome_broadcaster.mm
+++ b/ios/chrome/browser/ui/broadcaster/chrome_broadcaster.mm
@@ -317,6 +317,12 @@
   } else if (type == @encode(CGRect)) {
     CGRect rectValue = value.CGRectValue;
     [invocation setArgument:&rectValue atIndex:2];
+  } else if (type == @encode(CGSize)) {
+    CGSize sizeValue = value.CGSizeValue;
+    [invocation setArgument:&sizeValue atIndex:2];
+  } else if (type == @encode(UIEdgeInsets)) {
+    UIEdgeInsets insetValue = value.UIEdgeInsetsValue;
+    [invocation setArgument:&insetValue atIndex:2];
   } else if (type == @encode(int)) {
     DCHECK(valueAsNumber);
     int intValue = valueAsNumber.intValue;
diff --git a/ios/chrome/browser/ui/broadcaster/chrome_broadcaster_unittest.mm b/ios/chrome/browser/ui/broadcaster/chrome_broadcaster_unittest.mm
index 76e216e..42d999f 100644
--- a/ios/chrome/browser/ui/broadcaster/chrome_broadcaster_unittest.mm
+++ b/ios/chrome/browser/ui/broadcaster/chrome_broadcaster_unittest.mm
@@ -17,15 +17,25 @@
 @interface TestObserver : NSObject<ChromeBroadcastObserver>
 @property(nonatomic) BOOL lastObservedBool;
 @property(nonatomic) CGFloat lastObservedCGFloat;
+@property(nonatomic) CGSize lastObservedCGSize;
+@property(nonatomic) UIEdgeInsets lastObservedUIEdgeInsets;
 @property(nonatomic) NSInteger tabStripVisibleCallCount;
 @property(nonatomic) NSInteger contentScrollOffsetCallCount;
+@property(nonatomic) NSInteger scrollViewSizeCallCount;
+@property(nonatomic) NSInteger contentSizeCallCount;
+@property(nonatomic) NSInteger contentInsetCallCount;
 @end
 
 @implementation TestObserver
 @synthesize lastObservedBool = _lastObservedBool;
 @synthesize lastObservedCGFloat = _lastObservedCGFloat;
+@synthesize lastObservedCGSize = _lastObservedCGSize;
+@synthesize lastObservedUIEdgeInsets = _lastObservedUIEdgeInsets;
 @synthesize tabStripVisibleCallCount = _tabStripVisibleCallCount;
 @synthesize contentScrollOffsetCallCount = _contentScrollOffsetCallCount;
+@synthesize scrollViewSizeCallCount = _scrollViewSizeCallCount;
+@synthesize contentSizeCallCount = _contentSizeCallCount;
+@synthesize contentInsetCallCount = _contentInsetCallCount;
 
 - (void)broadcastScrollViewIsScrolling:(BOOL)visible {
   self.tabStripVisibleCallCount++;
@@ -37,15 +47,34 @@
   self.lastObservedCGFloat = offset;
 }
 
+- (void)broadcastScrollViewSize:(CGSize)scrollViewSize {
+  self.scrollViewSizeCallCount++;
+  self.lastObservedCGSize = scrollViewSize;
+}
+
+- (void)broadcastScrollViewContentSize:(CGSize)contentSize {
+  self.contentSizeCallCount++;
+  self.lastObservedCGSize = contentSize;
+}
+
+- (void)broadcastScrollViewContentInset:(UIEdgeInsets)contentInset {
+  self.contentInsetCallCount++;
+  self.lastObservedUIEdgeInsets = contentInset;
+}
+
 @end
 
 @interface TestObservable : NSObject
 @property(nonatomic) BOOL observableBool;
 @property(nonatomic) CGFloat observableCGFloat;
+@property(nonatomic) CGSize observableCGSize;
+@property(nonatomic) UIEdgeInsets observableUIEdgeInsets;
 @end
 @implementation TestObservable
 @synthesize observableBool = _observableBool;
 @synthesize observableCGFloat = _observableCGFloat;
+@synthesize observableCGSize = _observableCGSize;
+@synthesize observableUIEdgeInsets = _observableUIEdgeInsets;
 @end
 
 typedef PlatformTest ChromeBroadcasterTest;
@@ -147,6 +176,97 @@
   EXPECT_EQ(2, observer.contentScrollOffsetCallCount);
 }
 
+TEST_F(ChromeBroadcasterTest, TestObserveScrollViewSizeFirst) {
+  ChromeBroadcaster* broadcaster = [[ChromeBroadcaster alloc] init];
+  TestObserver* observer = [[TestObserver alloc] init];
+  EXPECT_TRUE(CGSizeEqualToSize(observer.lastObservedCGSize, CGSizeZero));
+  EXPECT_EQ(0, observer.scrollViewSizeCallCount);
+  [broadcaster addObserver:observer
+               forSelector:@selector(broadcastScrollViewSize:)];
+  EXPECT_TRUE(CGSizeEqualToSize(observer.lastObservedCGSize, CGSizeZero));
+  EXPECT_EQ(0, observer.scrollViewSizeCallCount);
+
+  TestObservable* observable = [[TestObservable alloc] init];
+  CGSize kScrollViewSize1 = CGSizeMake(100, 100);
+  observable.observableCGSize = kScrollViewSize1;
+  EXPECT_TRUE(CGSizeEqualToSize(observer.lastObservedCGSize, CGSizeZero));
+  EXPECT_EQ(0, observer.scrollViewSizeCallCount);
+
+  [broadcaster broadcastValue:@"observableCGSize"
+                     ofObject:observable
+                     selector:@selector(broadcastScrollViewSize:)];
+  EXPECT_TRUE(CGSizeEqualToSize(observer.lastObservedCGSize, kScrollViewSize1));
+  EXPECT_EQ(1, observer.scrollViewSizeCallCount);
+
+  CGSize kScrollViewSize2 = CGSizeMake(200, 200);
+  observable.observableCGSize = kScrollViewSize2;
+  EXPECT_TRUE(CGSizeEqualToSize(observer.lastObservedCGSize, kScrollViewSize2));
+  EXPECT_EQ(2, observer.scrollViewSizeCallCount);
+}
+
+TEST_F(ChromeBroadcasterTest, TestObserveContentSizeFirst) {
+  ChromeBroadcaster* broadcaster = [[ChromeBroadcaster alloc] init];
+  TestObserver* observer = [[TestObserver alloc] init];
+  EXPECT_TRUE(CGSizeEqualToSize(observer.lastObservedCGSize, CGSizeZero));
+  EXPECT_EQ(0, observer.contentSizeCallCount);
+  [broadcaster addObserver:observer
+               forSelector:@selector(broadcastScrollViewContentSize:)];
+  EXPECT_TRUE(CGSizeEqualToSize(observer.lastObservedCGSize, CGSizeZero));
+  EXPECT_EQ(0, observer.contentSizeCallCount);
+
+  TestObservable* observable = [[TestObservable alloc] init];
+  CGSize kContentViewSize1 = CGSizeMake(100, 100);
+  observable.observableCGSize = kContentViewSize1;
+  EXPECT_TRUE(CGSizeEqualToSize(observer.lastObservedCGSize, CGSizeZero));
+  EXPECT_EQ(0, observer.contentSizeCallCount);
+
+  [broadcaster broadcastValue:@"observableCGSize"
+                     ofObject:observable
+                     selector:@selector(broadcastScrollViewContentSize:)];
+  EXPECT_TRUE(
+      CGSizeEqualToSize(observer.lastObservedCGSize, kContentViewSize1));
+  EXPECT_EQ(1, observer.contentSizeCallCount);
+
+  CGSize kContentViewSize2 = CGSizeMake(200, 200);
+  observable.observableCGSize = kContentViewSize2;
+  EXPECT_TRUE(
+      CGSizeEqualToSize(observer.lastObservedCGSize, kContentViewSize2));
+  EXPECT_EQ(2, observer.contentSizeCallCount);
+}
+
+TEST_F(ChromeBroadcasterTest, TestObserveContentInsetFirst) {
+  ChromeBroadcaster* broadcaster = [[ChromeBroadcaster alloc] init];
+  TestObserver* observer = [[TestObserver alloc] init];
+  EXPECT_TRUE(UIEdgeInsetsEqualToEdgeInsets(observer.lastObservedUIEdgeInsets,
+                                            UIEdgeInsetsZero));
+  EXPECT_EQ(0, observer.contentInsetCallCount);
+  [broadcaster addObserver:observer
+               forSelector:@selector(broadcastScrollViewContentInset:)];
+  EXPECT_TRUE(UIEdgeInsetsEqualToEdgeInsets(observer.lastObservedUIEdgeInsets,
+                                            UIEdgeInsetsZero));
+  EXPECT_EQ(0, observer.contentInsetCallCount);
+
+  TestObservable* observable = [[TestObservable alloc] init];
+  UIEdgeInsets kInsets1 = UIEdgeInsetsMake(1, 1, 1, 1);
+  observable.observableUIEdgeInsets = kInsets1;
+  EXPECT_TRUE(UIEdgeInsetsEqualToEdgeInsets(observer.lastObservedUIEdgeInsets,
+                                            UIEdgeInsetsZero));
+  EXPECT_EQ(0, observer.contentInsetCallCount);
+
+  [broadcaster broadcastValue:@"observableUIEdgeInsets"
+                     ofObject:observable
+                     selector:@selector(broadcastScrollViewContentInset:)];
+  EXPECT_TRUE(UIEdgeInsetsEqualToEdgeInsets(observer.lastObservedUIEdgeInsets,
+                                            kInsets1));
+  EXPECT_EQ(1, observer.contentInsetCallCount);
+
+  UIEdgeInsets kInsets2 = UIEdgeInsetsMake(2, 2, 2, 2);
+  observable.observableUIEdgeInsets = kInsets2;
+  EXPECT_TRUE(UIEdgeInsetsEqualToEdgeInsets(observer.lastObservedUIEdgeInsets,
+                                            kInsets2));
+  EXPECT_EQ(2, observer.contentInsetCallCount);
+}
+
 TEST_F(ChromeBroadcasterTest, TestBroadcastManyFloats) {
   ChromeBroadcaster* broadcaster = [[ChromeBroadcaster alloc] init];
   NSMutableArray<TestObserver*>* observers = [[NSMutableArray alloc] init];
diff --git a/ios/chrome/browser/ui/fullscreen/fullscreen_controller_impl.mm b/ios/chrome/browser/ui/fullscreen/fullscreen_controller_impl.mm
index b169878..a1a6403 100644
--- a/ios/chrome/browser/ui/fullscreen/fullscreen_controller_impl.mm
+++ b/ios/chrome/browser/ui/fullscreen/fullscreen_controller_impl.mm
@@ -27,10 +27,18 @@
                     mediator:mediator_.get()]) {
   DCHECK(broadcaster_);
   [broadcaster_ addObserver:bridge_
+                forSelector:@selector(broadcastScrollViewSize:)];
+  [broadcaster_ addObserver:bridge_
+                forSelector:@selector(broadcastScrollViewContentSize:)];
+  [broadcaster_ addObserver:bridge_
+                forSelector:@selector(broadcastScrollViewContentInset:)];
+  [broadcaster_ addObserver:bridge_
                 forSelector:@selector(broadcastContentScrollOffset:)];
   [broadcaster_ addObserver:bridge_
                 forSelector:@selector(broadcastScrollViewIsScrolling:)];
   [broadcaster_ addObserver:bridge_
+                forSelector:@selector(broadcastScrollViewIsZooming:)];
+  [broadcaster_ addObserver:bridge_
                 forSelector:@selector(broadcastScrollViewIsDragging:)];
   [broadcaster_ addObserver:bridge_
                 forSelector:@selector(broadcastToolbarHeight:)];
@@ -81,10 +89,18 @@
   if (web_state_list_observer_)
     web_state_list_observer_->Disconnect();
   [broadcaster_ removeObserver:bridge_
+                   forSelector:@selector(broadcastScrollViewSize:)];
+  [broadcaster_ removeObserver:bridge_
+                   forSelector:@selector(broadcastScrollViewContentSize:)];
+  [broadcaster_ removeObserver:bridge_
+                   forSelector:@selector(broadcastScrollViewContentInset:)];
+  [broadcaster_ removeObserver:bridge_
                    forSelector:@selector(broadcastContentScrollOffset:)];
   [broadcaster_ removeObserver:bridge_
                    forSelector:@selector(broadcastScrollViewIsScrolling:)];
   [broadcaster_ removeObserver:bridge_
+                   forSelector:@selector(broadcastScrollViewIsZooming:)];
+  [broadcaster_ removeObserver:bridge_
                    forSelector:@selector(broadcastScrollViewIsDragging:)];
   [broadcaster_ removeObserver:bridge_
                    forSelector:@selector(broadcastToolbarHeight:)];
diff --git a/ios/chrome/browser/ui/fullscreen/fullscreen_model.h b/ios/chrome/browser/ui/fullscreen/fullscreen_model.h
index 46ac581..fb7b9f3 100644
--- a/ios/chrome/browser/ui/fullscreen/fullscreen_model.h
+++ b/ios/chrome/browser/ui/fullscreen/fullscreen_model.h
@@ -39,6 +39,9 @@
   // by navigations or toolbar height changes.
   bool has_base_offset() const { return !std::isnan(base_offset_); }
 
+  // The base offset against which the fullscreen progress is being calculated.
+  CGFloat base_offset() const { return base_offset_; }
+
   // Increments and decrements |disabled_counter_| for features that require the
   // toolbar be completely visible.
   void IncrementDisabledCounter();
@@ -56,6 +59,19 @@
   void SetToolbarHeight(CGFloat toolbar_height);
   CGFloat GetToolbarHeight() const;
 
+  // Setter for the height of the scroll view displaying the main content.
+  void SetScrollViewHeight(CGFloat scroll_view_height);
+  CGFloat GetScrollViewHeight() const;
+
+  // Setter for the current height of the rendered page.
+  void SetContentHeight(CGFloat content_height);
+  CGFloat GetContentHeight() const;
+
+  // Setter for the top content inset of the scroll view displaying the main
+  // content.
+  void SetTopContentInset(CGFloat top_inset);
+  CGFloat GetTopContentInset() const;
+
   // Setter for the current vertical content offset.  Setting this will
   // recalculate the progress value.
   void SetYContentOffset(CGFloat y_content_offset);
@@ -65,24 +81,46 @@
   // and the progress value is not 0.0 or 1.0, the model will round to the
   // nearest value.
   void SetScrollViewIsScrolling(bool scrolling);
-  bool ISScrollViewScrolling() const;
+  bool IsScrollViewScrolling() const;
+
+  // Setter for whether the scroll view is zooming.
+  void SetScrollViewIsZooming(bool zooming);
+  bool IsScrollViewZooming() const;
 
   // Setter for whether the scroll view is being dragged.
   void SetScrollViewIsDragging(bool dragging);
   bool IsScrollViewDragging() const;
 
  private:
-  // Setter for |progress_|.  Notifies observers of the new value if
-  // |notify_observers| is true.
-  void SetProgress(CGFloat progress);
+  // Returns how a scroll to the current |y_content_offset_| from |from_offset|
+  // should be handled.
+  enum class ScrollAction : short {
+    kIgnore,                       // Ignore the scroll.
+    kUpdateBaseOffset,             // Update |base_offset_| only.
+    kUpdateProgress,               // Update |progress_| only.
+    kUpdateBaseOffsetAndProgress,  // Update |bse_offset_| and |progress_|.
+  };
+  ScrollAction ActionForScrollFromOffset(CGFloat from_offset) const;
 
   // Updates the base offset given the current y content offset, progress, and
   // toolbar height.
   void UpdateBaseOffset();
 
+  // Updates the progress value given the current y content offset, base offset,
+  // and toolbar height.
+  void UpdateProgress();
+
+  // Setter for |progress_|.  Notifies observers of the new value if
+  // |notify_observers| is true.
+  void SetProgress(CGFloat progress);
+
   // ChromeBroadcastObserverInterface:
+  void OnScrollViewSizeBroadcasted(CGSize scroll_view_size) override;
+  void OnScrollViewContentSizeBroadcasted(CGSize content_size) override;
+  void OnScrollViewContentInsetBroadcasted(UIEdgeInsets content_inset) override;
   void OnContentScrollOffsetBroadcasted(CGFloat offset) override;
   void OnScrollViewIsScrollingBroadcasted(bool scrolling) override;
+  void OnScrollViewIsZoomingBroadcasted(bool zooming) override;
   void OnScrollViewIsDraggingBroadcasted(bool dragging) override;
   void OnToolbarHeightBroadcasted(CGFloat toolbar_height) override;
 
@@ -98,10 +136,18 @@
   CGFloat toolbar_height_ = 0.0;
   // The current vertical content offset of the main content.
   CGFloat y_content_offset_ = 0.0;
+  // The height of the scroll view displaying the current page.
+  CGFloat scroll_view_height_ = 0.0;
+  // The height of the current page's rendered content.
+  CGFloat content_height_ = 0.0;
+  // The top inset of the scroll view displaying the current page.
+  CGFloat top_inset_ = 0.0;
   // How many currently-running features require the toolbar be visible.
   size_t disabled_counter_ = 0;
   // Whether the main content is being scrolled.
   bool scrolling_ = false;
+  // Whether the scroll view is zooming.
+  bool zooming_ = false;
   // Whether the main content is being dragged.
   bool dragging_ = false;
   // The number of FullscreenModelObserver callbacks currently being executed.
diff --git a/ios/chrome/browser/ui/fullscreen/fullscreen_model.mm b/ios/chrome/browser/ui/fullscreen/fullscreen_model.mm
index 93521e6..3d261b6 100644
--- a/ios/chrome/browser/ui/fullscreen/fullscreen_model.mm
+++ b/ios/chrome/browser/ui/fullscreen/fullscreen_model.mm
@@ -40,7 +40,7 @@
     // Fullscreen observers are expected to show the toolbar when fullscreen is
     // disabled. Update the internal state to match this.
     SetProgress(1.0);
-    base_offset_ = NAN;
+    UpdateBaseOffset();
   }
 }
 
@@ -84,21 +84,47 @@
   return toolbar_height_;
 }
 
+void FullscreenModel::SetScrollViewHeight(CGFloat scroll_view_height) {
+  scroll_view_height_ = scroll_view_height;
+}
+
+CGFloat FullscreenModel::GetScrollViewHeight() const {
+  return scroll_view_height_;
+}
+
+void FullscreenModel::SetContentHeight(CGFloat content_height) {
+  content_height_ = content_height;
+}
+
+CGFloat FullscreenModel::GetContentHeight() const {
+  return content_height_;
+}
+
+void FullscreenModel::SetTopContentInset(CGFloat top_inset) {
+  top_inset_ = top_inset;
+}
+
+CGFloat FullscreenModel::GetTopContentInset() const {
+  return top_inset_;
+}
+
 void FullscreenModel::SetYContentOffset(CGFloat y_content_offset) {
-
+  CGFloat from_offset = y_content_offset_;
   y_content_offset_ = y_content_offset;
-
-  if (!has_base_offset())
-    UpdateBaseOffset();
-
-  if (!enabled())
-    return;
-
-  if (scrolling_ && !observer_callback_count_) {
-    CGFloat delta = base_offset_ - y_content_offset_;
-    SetProgress(1.0 + delta / toolbar_height_);
-  } else {
-    UpdateBaseOffset();
+  switch (ActionForScrollFromOffset(from_offset)) {
+    case ScrollAction::kUpdateBaseOffset:
+      UpdateBaseOffset();
+      break;
+    case ScrollAction::kUpdateProgress:
+      UpdateProgress();
+      break;
+    case ScrollAction::kUpdateBaseOffsetAndProgress:
+      UpdateBaseOffset();
+      UpdateProgress();
+      break;
+    case ScrollAction::kIgnore:
+      // no op.
+      break;
   }
 }
 
@@ -118,10 +144,18 @@
   }
 }
 
-bool FullscreenModel::ISScrollViewScrolling() const {
+bool FullscreenModel::IsScrollViewScrolling() const {
   return scrolling_;
 }
 
+void FullscreenModel::SetScrollViewIsZooming(bool zooming) {
+  zooming_ = zooming;
+}
+
+bool FullscreenModel::IsScrollViewZooming() const {
+  return zooming_;
+}
+
 void FullscreenModel::SetScrollViewIsDragging(bool dragging) {
   if (dragging_ == dragging)
     return;
@@ -139,6 +173,51 @@
   return dragging_;
 }
 
+FullscreenModel::ScrollAction FullscreenModel::ActionForScrollFromOffset(
+    CGFloat from_offset) const {
+  // Update the base offset but don't recalculate progress if:
+  // - the model is disabled,
+  // - the scroll is not triggered by a user action,
+  // - the sroll view is zooming,
+  // - the scroll is triggered from a FullscreenModelObserver callback,
+  // - there is no toolbar,
+  // - the scroll offset doesn't change.
+  if (!enabled() || !scrolling_ || zooming_ || observer_callback_count_ ||
+      AreCGFloatsEqual(toolbar_height_, 0.0) ||
+      AreCGFloatsEqual(y_content_offset_, from_offset)) {
+    return ScrollAction::kUpdateBaseOffset;
+  }
+
+  // Ignore if:
+  // - the scroll is a bounce-up animation at the top,
+  // - the scroll is a bounce-down animation at the bottom,
+  // - the scroll is attempting to scroll content up when it already fits.
+  bool scrolling_content_down = y_content_offset_ - from_offset < 0.0;
+  bool scrolling_past_top = y_content_offset_ <= -top_inset_;
+  bool scrolling_past_bottom =
+      y_content_offset_ + scroll_view_height_ >= content_height_;
+  bool content_fits = content_height_ <= scroll_view_height_ - top_inset_;
+  if ((scrolling_past_top && !scrolling_content_down) ||
+      (scrolling_past_bottom && scrolling_content_down) ||
+      (content_fits && !scrolling_content_down)) {
+    return ScrollAction::kIgnore;
+  }
+
+  // All other scrolls should result in an updated progress value.  If the model
+  // doesn't have a base offset, it should also be updated.
+  return has_base_offset() ? ScrollAction::kUpdateProgress
+                           : ScrollAction::kUpdateBaseOffsetAndProgress;
+}
+
+void FullscreenModel::UpdateProgress() {
+  CGFloat delta = base_offset_ - y_content_offset_;
+  SetProgress(1.0 + delta / toolbar_height_);
+}
+
+void FullscreenModel::UpdateBaseOffset() {
+  base_offset_ = y_content_offset_ - (1.0 - progress_) * toolbar_height_;
+}
+
 void FullscreenModel::SetProgress(CGFloat progress) {
   progress = std::min(static_cast<CGFloat>(1.0), progress);
   progress = std::max(static_cast<CGFloat>(0.0), progress);
@@ -152,8 +231,17 @@
   }
 }
 
-void FullscreenModel::UpdateBaseOffset() {
-  base_offset_ = y_content_offset_ - (1.0 - progress_) * toolbar_height_;
+void FullscreenModel::OnScrollViewSizeBroadcasted(CGSize scroll_view_size) {
+  SetScrollViewHeight(scroll_view_size.height);
+}
+
+void FullscreenModel::OnScrollViewContentSizeBroadcasted(CGSize content_size) {
+  SetContentHeight(content_size.height);
+}
+
+void FullscreenModel::OnScrollViewContentInsetBroadcasted(
+    UIEdgeInsets content_inset) {
+  SetTopContentInset(content_inset.top);
 }
 
 void FullscreenModel::OnContentScrollOffsetBroadcasted(CGFloat offset) {
@@ -164,6 +252,10 @@
   SetScrollViewIsScrolling(scrolling);
 }
 
+void FullscreenModel::OnScrollViewIsZoomingBroadcasted(bool zooming) {
+  SetScrollViewIsZooming(zooming);
+}
+
 void FullscreenModel::OnScrollViewIsDraggingBroadcasted(bool dragging) {
   SetScrollViewIsDragging(dragging);
 }
diff --git a/ios/chrome/browser/ui/fullscreen/fullscreen_model_unittest.mm b/ios/chrome/browser/ui/fullscreen/fullscreen_model_unittest.mm
index 949921f..fff3815 100644
--- a/ios/chrome/browser/ui/fullscreen/fullscreen_model_unittest.mm
+++ b/ios/chrome/browser/ui/fullscreen/fullscreen_model_unittest.mm
@@ -16,6 +16,10 @@
 namespace {
 // The toolbar height to use for tests.
 const CGFloat kToolbarHeight = 50.0;
+// The scroll view height used for tests.
+const CGFloat kScrollViewHeight = 400.0;
+// The content height used for tests.
+const CGFloat kContentHeight = 5000.0;
 }  // namespace
 
 // Test fixture for FullscreenModel.
@@ -26,6 +30,8 @@
     // Set the toolbar height to kToolbarHeight, and simulate a page load that
     // finishes with a 0.0 y content offset.
     model_.SetToolbarHeight(kToolbarHeight);
+    model_.SetScrollViewHeight(kScrollViewHeight);
+    model_.SetContentHeight(kContentHeight);
     model_.ResetForNavigation();
     model_.SetYContentOffset(0.0);
   }
@@ -56,7 +62,8 @@
   // Since the model has been disabled the Toolbar is shown, verify that the
   // model state reflects that.
   EXPECT_EQ(observer().progress(), 1.0);
-  EXPECT_FALSE(model().has_base_offset());
+  EXPECT_EQ(model().base_offset(),
+            GetFullscreenBaseOffsetForProgress(&model(), 1.0));
   // Increment again and check that the model is still disabled.
   model().IncrementDisabledCounter();
   EXPECT_FALSE(model().enabled());
@@ -129,15 +136,47 @@
   EXPECT_EQ(model().GetYContentOffset(), kFinalProgress * kToolbarHeight);
 }
 
+// Tests that updating the y content offset of a disabled model only updates its
+// base offset.
+TEST_F(FullscreenModelTest, DisabledScroll) {
+  const CGFloat kProgress = 0.5;
+  model().IncrementDisabledCounter();
+  SimulateFullscreenUserScrollForProgress(&model(), kProgress);
+  EXPECT_EQ(observer().progress(), 1.0);
+  EXPECT_EQ(model().base_offset(),
+            GetFullscreenBaseOffsetForProgress(&model(), 1.0));
+}
+
 // Tests that updating the y content offset programmatically (i.e. while the
-// scroll view is not scrolling) does not produce a new progress value for
-// observers.
+// scroll view is not scrolling) only updates the base offset.
 TEST_F(FullscreenModelTest, ProgrammaticScroll) {
   // Perform a programmatic scroll that would result in a progress of 0.5, and
   // verify that the initial progress value of 1.0 is maintained.
   const CGFloat kProgress = 0.5;
   model().SetYContentOffset(kProgress * kToolbarHeight);
   EXPECT_EQ(observer().progress(), 1.0);
+  EXPECT_EQ(model().base_offset(),
+            GetFullscreenBaseOffsetForProgress(&model(), 1.0));
+}
+
+// Tests that updating the y content offset while zooming only updates the
+// model's base offset.
+TEST_F(FullscreenModelTest, ZoomScroll) {
+  const CGFloat kProgress = 0.5;
+  model().SetScrollViewIsZooming(true);
+  SimulateFullscreenUserScrollForProgress(&model(), kProgress);
+  EXPECT_EQ(observer().progress(), 1.0);
+  EXPECT_EQ(model().base_offset(),
+            GetFullscreenBaseOffsetForProgress(&model(), 1.0));
+}
+
+// Tests that updating the y content offset while the toolbar height is 0 only
+// updates the model's base offset.
+TEST_F(FullscreenModelTest, NoToolbarScroll) {
+  model().SetToolbarHeight(0.0);
+  model().SetYContentOffset(100);
+  EXPECT_EQ(observer().progress(), 1.0);
+  EXPECT_EQ(model().base_offset(), 100);
 }
 
 // Tests that setting scrolling to false sends a scroll end signal to its
diff --git a/ios/chrome/browser/ui/fullscreen/test/fullscreen_model_test_util.mm b/ios/chrome/browser/ui/fullscreen/test/fullscreen_model_test_util.mm
index b00ff8c2..997bacb 100644
--- a/ios/chrome/browser/ui/fullscreen/test/fullscreen_model_test_util.mm
+++ b/ios/chrome/browser/ui/fullscreen/test/fullscreen_model_test_util.mm
@@ -15,6 +15,8 @@
                                     CGFloat toolbar_height) {
   EXPECT_GE(toolbar_height, 0.0);
   model->SetToolbarHeight(toolbar_height);
+  model->SetScrollViewHeight(2 * toolbar_height);
+  model->SetContentHeight(2 * model->GetScrollViewHeight());
   model->ResetForNavigation();
   model->SetYContentOffset(0.0);
 }
diff --git a/ios/chrome/browser/ui/main_content/main_content_ui_broadcasting_util.mm b/ios/chrome/browser/ui/main_content/main_content_ui_broadcasting_util.mm
index 224e464..e4d8e5d 100644
--- a/ios/chrome/browser/ui/main_content/main_content_ui_broadcasting_util.mm
+++ b/ios/chrome/browser/ui/main_content/main_content_ui_broadcasting_util.mm
@@ -14,22 +14,41 @@
 
 void StartBroadcastingMainContentUI(id<MainContentUI> main_content,
                                     ChromeBroadcaster* broadcaster) {
+  [broadcaster broadcastValue:@"scrollViewSize"
+                     ofObject:main_content.mainContentUIState
+                     selector:@selector(broadcastScrollViewSize:)];
+  [broadcaster broadcastValue:@"contentSize"
+                     ofObject:main_content.mainContentUIState
+                     selector:@selector(broadcastScrollViewContentSize:)];
+  [broadcaster broadcastValue:@"contentInset"
+                     ofObject:main_content.mainContentUIState
+                     selector:@selector(broadcastScrollViewContentInset:)];
   [broadcaster broadcastValue:@"yContentOffset"
                      ofObject:main_content.mainContentUIState
                      selector:@selector(broadcastContentScrollOffset:)];
   [broadcaster broadcastValue:@"scrolling"
                      ofObject:main_content.mainContentUIState
                      selector:@selector(broadcastScrollViewIsScrolling:)];
+  [broadcaster broadcastValue:@"zooming"
+                     ofObject:main_content.mainContentUIState
+                     selector:@selector(broadcastScrollViewIsZooming:)];
   [broadcaster broadcastValue:@"dragging"
                      ofObject:main_content.mainContentUIState
                      selector:@selector(broadcastScrollViewIsDragging:)];
 }
 
 void StopBroadcastingMainContentUI(ChromeBroadcaster* broadcaster) {
+  [broadcaster stopBroadcastingForSelector:@selector(broadcastScrollViewSize:)];
+  [broadcaster
+      stopBroadcastingForSelector:@selector(broadcastScrollViewContentSize:)];
+  [broadcaster
+      stopBroadcastingForSelector:@selector(broadcastScrollViewContentInset:)];
   [broadcaster
       stopBroadcastingForSelector:@selector(broadcastContentScrollOffset:)];
   [broadcaster
       stopBroadcastingForSelector:@selector(broadcastScrollViewIsScrolling:)];
   [broadcaster
+      stopBroadcastingForSelector:@selector(broadcastScrollViewIsZooming:)];
+  [broadcaster
       stopBroadcastingForSelector:@selector(broadcastScrollViewIsDragging:)];
 }
diff --git a/ios/chrome/browser/ui/main_content/main_content_ui_state.h b/ios/chrome/browser/ui/main_content/main_content_ui_state.h
index f789265..22f4f0b 100644
--- a/ios/chrome/browser/ui/main_content/main_content_ui_state.h
+++ b/ios/chrome/browser/ui/main_content/main_content_ui_state.h
@@ -10,12 +10,24 @@
 // An object encapsulating the broadcasted state of the main scrollable content.
 @interface MainContentUIState : NSObject
 
+// The size of the scroll view displaying the main content.
+// This should be broadcast using |-broadcastScrollViewSize:|.
+@property(nonatomic, readonly) CGSize scrollViewSize;
+// The height of the current page's rendered content.
+// This should be broadcast using |-broadcastScrollViewContentSize:|.
+@property(nonatomic, readonly) CGSize contentSize;
+// The content inset of the scroll view displaying the main content.
+// This should be broadcast using |-broadcastScrollViewContentInset:|.
+@property(nonatomic, readonly) UIEdgeInsets contentInset;
 // The vertical offset of the main content.
 // This should be broadcast using |-broadcastContentScrollOffset:|.
 @property(nonatomic, readonly) CGFloat yContentOffset;
 // Whether the scroll view is currently scrolling.
 // This should be broadcast using |-broadcastScrollViewIsScrolling:|.
 @property(nonatomic, readonly, getter=isScrolling) BOOL scrolling;
+// Whether the scroll view is currently zooming.
+// This should be broadcast using |-broadcastScrollViewIsZooming:|.
+@property(nonatomic, readonly, getter=isZooming) BOOL zooming;
 // Whether the scroll view is currently being dragged.
 // This should be broadcast using |-broadcastScrollViewIsDragging:|.
 @property(nonatomic, readonly, getter=isDragging) BOOL dragging;
@@ -33,6 +45,12 @@
     NS_DESIGNATED_INITIALIZER;
 - (nullable instancetype)init NS_UNAVAILABLE;
 
+// Called to broadcast changes in the scroll view's size.
+- (void)scrollViewSizeDidChange:(CGSize)scrollViewSize;
+// Called to broadcast changes in the content size.
+- (void)scrollViewDidResetContentSize:(CGSize)contentSize;
+// Called to broadcast changes in the content inset.
+- (void)scrollViewDidResetContentInset:(UIEdgeInsets)contentInset;
 // Called to broadcast scroll offset changes due to scrolling.
 - (void)scrollViewDidScrollToOffset:(CGPoint)offset;
 // Called when a drag event with |panGesture| begins.
@@ -45,6 +63,9 @@
                            residualVelocity:(CGPoint)velocity;
 // Called when the scroll view stops decelerating.
 - (void)scrollViewDidEndDecelerating;
+// Called when the scroll view starts and ends zooming.
+- (void)scrollViewDidStartZooming;
+- (void)scrollViewDidEndZooming;
 // Called when a scroll event is interrupted (i.e. when a navigation occurs mid-
 // scroll).
 - (void)scrollWasInterrupted;
diff --git a/ios/chrome/browser/ui/main_content/main_content_ui_state.mm b/ios/chrome/browser/ui/main_content/main_content_ui_state.mm
index a25607d..a1a72ae 100644
--- a/ios/chrome/browser/ui/main_content/main_content_ui_state.mm
+++ b/ios/chrome/browser/ui/main_content/main_content_ui_state.mm
@@ -13,8 +13,12 @@
 
 @interface MainContentUIState ()
 // Redefine broadcast properties as readwrite.
+@property(nonatomic, assign) CGSize scrollViewSize;
+@property(nonatomic, assign) CGSize contentSize;
+@property(nonatomic, assign) UIEdgeInsets contentInset;
 @property(nonatomic, assign) CGFloat yContentOffset;
 @property(nonatomic, assign, getter=isScrolling) BOOL scrolling;
+@property(nonatomic, assign, getter=isZooming) BOOL zooming;
 @property(nonatomic, assign, getter=isDragging) BOOL dragging;
 // Whether the scroll view is decelerating.
 @property(nonatomic, assign, getter=isDecelerating) BOOL decelerating;
@@ -25,8 +29,12 @@
 @end
 
 @implementation MainContentUIState
+@synthesize scrollViewSize = _scrollViewSize;
+@synthesize contentSize = _contentSize;
+@synthesize contentInset = _contentInset;
 @synthesize yContentOffset = _yContentOffset;
 @synthesize scrolling = _scrolling;
+@synthesize zooming = _zooming;
 @synthesize dragging = _dragging;
 @synthesize decelerating = _decelerating;
 
@@ -82,6 +90,18 @@
 
 #pragma mark Public
 
+- (void)scrollViewSizeDidChange:(CGSize)scrollViewSize {
+  self.state.scrollViewSize = scrollViewSize;
+}
+
+- (void)scrollViewDidResetContentSize:(CGSize)contentSize {
+  self.state.contentSize = contentSize;
+}
+
+- (void)scrollViewDidResetContentInset:(UIEdgeInsets)contentInset {
+  self.state.contentInset = contentInset;
+}
+
 - (void)scrollViewDidScrollToOffset:(CGPoint)offset {
   self.state.yContentOffset = offset.y;
 }
@@ -110,10 +130,19 @@
   self.state.decelerating = NO;
 }
 
+- (void)scrollViewDidStartZooming {
+  self.state.zooming = YES;
+}
+
+- (void)scrollViewDidEndZooming {
+  self.state.zooming = NO;
+}
+
 - (void)scrollWasInterrupted {
   self.state.scrolling = NO;
   self.state.dragging = NO;
   self.state.decelerating = NO;
+  self.state.zooming = NO;
 }
 
 @end
diff --git a/ios/chrome/browser/ui/main_content/web_scroll_view_main_content_ui_forwarder.mm b/ios/chrome/browser/ui/main_content/web_scroll_view_main_content_ui_forwarder.mm
index 01074de..a163285 100644
--- a/ios/chrome/browser/ui/main_content/web_scroll_view_main_content_ui_forwarder.mm
+++ b/ios/chrome/browser/ui/main_content/web_scroll_view_main_content_ui_forwarder.mm
@@ -127,6 +127,11 @@
 
 #pragma mark CRWWebViewScrollViewObserver
 
+- (void)webViewScrollViewFrameDidChange:
+    (CRWWebViewScrollViewProxy*)webViewScrollViewProxy {
+  [self.updater scrollViewSizeDidChange:webViewScrollViewProxy.frame.size];
+}
+
 - (void)webViewScrollViewDidScroll:
     (CRWWebViewScrollViewProxy*)webViewScrollViewProxy {
   [self.updater scrollViewDidScrollToOffset:self.proxy.contentOffset];
@@ -152,6 +157,18 @@
   [self.updater scrollViewDidEndDecelerating];
 }
 
+- (void)webViewScrollViewDidResetContentSize:
+    (CRWWebViewScrollViewProxy*)webViewScrollViewProxy {
+  [self.updater
+      scrollViewDidResetContentSize:webViewScrollViewProxy.contentSize];
+}
+
+- (void)webViewScrollViewDidResetContentInset:
+    (CRWWebViewScrollViewProxy*)webViewScrollViewProxy {
+  [self.updater
+      scrollViewDidResetContentInset:webViewScrollViewProxy.contentInset];
+}
+
 #pragma mark - WebStateListObserving
 
 - (void)webStateList:(WebStateList*)webStateList
diff --git a/ios/web/public/web_state/ui/crw_web_view_scroll_view_proxy.h b/ios/web/public/web_state/ui/crw_web_view_scroll_view_proxy.h
index e3605b2..54cbbea 100644
--- a/ios/web/public/web_state/ui/crw_web_view_scroll_view_proxy.h
+++ b/ios/web/public/web_state/ui/crw_web_view_scroll_view_proxy.h
@@ -65,6 +65,8 @@
 // information look at the UIScrollViewDelegate documentation.
 @protocol CRWWebViewScrollViewObserver<NSObject>
 @optional
+- (void)webViewScrollViewFrameDidChange:
+    (CRWWebViewScrollViewProxy*)webViewScrollViewProxy;
 - (void)webViewScrollViewDidScroll:
     (CRWWebViewScrollViewProxy*)webViewScrollViewProxy;
 - (void)webViewScrollViewWillBeginDragging:
@@ -86,12 +88,18 @@
     (CRWWebViewScrollViewProxy*)webViewScrollViewProxy;
 - (void)webViewScrollViewDidResetContentSize:
     (CRWWebViewScrollViewProxy*)webViewScrollViewProxy;
+- (void)webViewScrollViewDidResetContentInset:
+    (CRWWebViewScrollViewProxy*)webViewScrollViewProxy;
 
 // The equivalent in UIScrollViewDelegate also takes a parameter (UIView*)view,
 // but CRWWebViewScrollViewObserver doesn't expose it for flexibility of future
 // implementation.
 - (void)webViewScrollViewWillBeginZooming:
     (CRWWebViewScrollViewProxy*)webViewScrollViewProxy;
+- (void)webViewScrollViewDidEndZooming:
+            (CRWWebViewScrollViewProxy*)webViewScrollViewProxy
+                               atScale:(CGFloat)scale;
+
 @end
 
 // A protocol to be implemented by objects to listen for changes to the
diff --git a/ios/web/web_state/ui/crw_web_view_scroll_view_proxy.mm b/ios/web/web_state/ui/crw_web_view_scroll_view_proxy.mm
index 31f1b96..ccdedcf 100644
--- a/ios/web/web_state/ui/crw_web_view_scroll_view_proxy.mm
+++ b/ios/web/web_state/ui/crw_web_view_scroll_view_proxy.mm
@@ -269,10 +269,17 @@
   [_observers webViewScrollViewWillBeginZooming:self];
 }
 
+- (void)scrollViewDidEndZooming:(UIScrollView*)scrollView
+                       withView:(UIView*)view
+                        atScale:(CGFloat)scale {
+  DCHECK_EQ(_scrollView, scrollView);
+  [_observers webViewScrollViewDidEndZooming:self atScale:scale];
+}
+
 #pragma mark -
 
 + (NSArray*)scrollViewObserverKeyPaths {
-  return @[ @"contentSize" ];
+  return @[ @"frame", @"contentSize", @"contentInset" ];
 }
 
 - (void)startObservingScrollView:(UIScrollView*)scrollView {
@@ -290,8 +297,12 @@
                         change:(NSDictionary*)change
                        context:(void*)context {
   DCHECK_EQ(object, _scrollView);
+  if ([keyPath isEqualToString:@"frame"])
+    [_observers webViewScrollViewFrameDidChange:self];
   if ([keyPath isEqualToString:@"contentSize"])
     [_observers webViewScrollViewDidResetContentSize:self];
+  if ([keyPath isEqualToString:@"contentInset"])
+    [_observers webViewScrollViewDidResetContentInset:self];
 }
 
 @end
diff --git a/ios/web/web_state/ui/crw_web_view_scroll_view_proxy_unittest.mm b/ios/web/web_state/ui/crw_web_view_scroll_view_proxy_unittest.mm
index 127571d..2c2f916 100644
--- a/ios/web/web_state/ui/crw_web_view_scroll_view_proxy_unittest.mm
+++ b/ios/web/web_state/ui/crw_web_view_scroll_view_proxy_unittest.mm
@@ -24,6 +24,9 @@
     mockScrollView_ = [OCMockObject niceMockForClass:[UIScrollView class]];
     webViewScrollViewProxy_ = [[CRWWebViewScrollViewProxy alloc] init];
   }
+  ~CRWWebViewScrollViewProxyTest() override {
+    [webViewScrollViewProxy_ setScrollView:nil];
+  }
   id mockScrollView_;
   CRWWebViewScrollViewProxy* webViewScrollViewProxy_;
 };
@@ -244,4 +247,32 @@
   }
 }
 
+// Tests that frame changes are communicated to observers.
+TEST_F(CRWWebViewScrollViewProxyTest, FrameDidChange) {
+  UIScrollView* scroll_view = [[UIScrollView alloc] initWithFrame:CGRectZero];
+  [webViewScrollViewProxy_ setScrollView:scroll_view];
+  id mock_delegate = [OCMockObject
+      niceMockForProtocol:@protocol(CRWWebViewScrollViewProxyObserver)];
+  [webViewScrollViewProxy_ addObserver:mock_delegate];
+  [[mock_delegate expect]
+      webViewScrollViewFrameDidChange:webViewScrollViewProxy_];
+  scroll_view.frame = CGRectMake(1, 2, 3, 4);
+  [mock_delegate verify];
+  [webViewScrollViewProxy_ setScrollView:nil];
+}
+
+// Tests that contentInset changes are communicated to observers.
+TEST_F(CRWWebViewScrollViewProxyTest, ContentInsetDidChange) {
+  UIScrollView* scroll_view = [[UIScrollView alloc] initWithFrame:CGRectZero];
+  [webViewScrollViewProxy_ setScrollView:scroll_view];
+  id mock_delegate = [OCMockObject
+      niceMockForProtocol:@protocol(CRWWebViewScrollViewProxyObserver)];
+  [webViewScrollViewProxy_ addObserver:mock_delegate];
+  [[mock_delegate expect]
+      webViewScrollViewDidResetContentInset:webViewScrollViewProxy_];
+  scroll_view.contentInset = UIEdgeInsetsMake(0, 1, 2, 3);
+  [mock_delegate verify];
+  [webViewScrollViewProxy_ setScrollView:nil];
+}
+
 }  // namespace