// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/web/public/ui/crw_web_view_scroll_view_proxy.h"
#import <UIKit/UIKit.h>
#import "base/test/scoped_feature_list.h"
#import "ios/web/common/features.h"
#import "ios/web/web_state/ui/crw_web_view_scroll_view_delegate_proxy.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "third_party/ocmock/gtest_support.h"
// TODO(crbug.com/40661733): Rewrite tests Delegate, MultipleScrollView,
// DelegateClearingUp not to depend on this, and delete this.
@interface CRWWebViewScrollViewProxy (Testing)
@property(nonatomic, readonly) CRWWebViewScrollViewDelegateProxy* delegateProxy;
@end
@interface UIScrollView (TestingCategory)
- (int)crw_categoryMethod;
@end
@implementation UIScrollView (TestingCategory)
- (int)crw_categoryMethod {
return 1;
}
@end
@interface CRWTestObserver : NSObject
- (instancetype)initWithProxy:(CRWWebViewScrollViewProxy*)proxy
NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
@end
@implementation CRWTestObserver {
CRWWebViewScrollViewProxy* _proxy;
}
- (instancetype)initWithProxy:(CRWWebViewScrollViewProxy*)proxy {
self = [super init];
if (self) {
_proxy = proxy;
[_proxy addObserver:self
forKeyPath:@"contentSize"
options:NSKeyValueObservingOptionNew
context:nullptr];
}
return self;
}
- (void)dealloc {
[_proxy removeObserver:self forKeyPath:@"contentSize"];
}
@end
namespace {
class CRWWebViewScrollViewProxyTest : public PlatformTest {
protected:
void SetUp() override {
mock_underlying_scroll_view_ = OCMClassMock([UIScrollView class]);
web_view_scroll_view_proxy_ = [[CRWWebViewScrollViewProxy alloc] init];
}
~CRWWebViewScrollViewProxyTest() override {
[web_view_scroll_view_proxy_ setScrollView:nil];
}
id mock_underlying_scroll_view_;
CRWWebViewScrollViewProxy* web_view_scroll_view_proxy_;
};
// Tests that the UIScrollViewDelegate is set correctly.
TEST_F(CRWWebViewScrollViewProxyTest, Delegate) {
OCMExpect([static_cast<UIScrollView*>(mock_underlying_scroll_view_)
setDelegate:web_view_scroll_view_proxy_.delegateProxy]);
[web_view_scroll_view_proxy_ setScrollView:mock_underlying_scroll_view_];
EXPECT_OCMOCK_VERIFY(mock_underlying_scroll_view_);
}
// Tests that setting 2 scroll views consecutively, clears the delegate of the
// previous scroll view.
TEST_F(CRWWebViewScrollViewProxyTest, MultipleScrollView) {
UIScrollView* mock_scroll_view1 = [[UIScrollView alloc] init];
UIScrollView* mock_scroll_view2 = [[UIScrollView alloc] init];
[web_view_scroll_view_proxy_ setScrollView:mock_scroll_view1];
[web_view_scroll_view_proxy_ setScrollView:mock_scroll_view2];
EXPECT_FALSE([mock_scroll_view1 delegate]);
EXPECT_EQ(web_view_scroll_view_proxy_.delegateProxy,
[mock_scroll_view2 delegate]);
[web_view_scroll_view_proxy_ setScrollView:nil];
}
// Tests that when releasing a scroll view from the CRWWebViewScrollViewProxy,
// the UIScrollView's delegate is also cleared.
TEST_F(CRWWebViewScrollViewProxyTest, DelegateClearingUp) {
UIScrollView* mock_scroll_view1 = [[UIScrollView alloc] init];
[web_view_scroll_view_proxy_ setScrollView:mock_scroll_view1];
EXPECT_EQ(web_view_scroll_view_proxy_.delegateProxy,
[mock_scroll_view1 delegate]);
[web_view_scroll_view_proxy_ setScrollView:nil];
EXPECT_FALSE([mock_scroll_view1 delegate]);
}
// Tests that CRWWebViewScrollViewProxy returns the correct property values from
// the underlying UIScrollView.
TEST_F(CRWWebViewScrollViewProxyTest, ScrollViewPresent) {
[web_view_scroll_view_proxy_ setScrollView:mock_underlying_scroll_view_];
OCMStub([mock_underlying_scroll_view_ isZooming]).andReturn(YES);
EXPECT_TRUE([web_view_scroll_view_proxy_ isZooming]);
// Arbitrary point.
const CGPoint point = CGPointMake(10, 10);
OCMStub([mock_underlying_scroll_view_ contentOffset]).andReturn(point);
EXPECT_TRUE(
CGPointEqualToPoint(point, [web_view_scroll_view_proxy_ contentOffset]));
// Arbitrary inset.
const UIEdgeInsets content_inset = UIEdgeInsetsMake(10, 10, 10, 10);
OCMStub([mock_underlying_scroll_view_ contentInset]).andReturn(content_inset);
EXPECT_TRUE(UIEdgeInsetsEqualToEdgeInsets(
content_inset, [web_view_scroll_view_proxy_ contentInset]));
// Arbitrary inset.
const UIEdgeInsets scroll_indicator_insets = UIEdgeInsetsMake(20, 20, 20, 20);
OCMStub([mock_underlying_scroll_view_ scrollIndicatorInsets])
.andReturn(scroll_indicator_insets);
EXPECT_TRUE(UIEdgeInsetsEqualToEdgeInsets(
scroll_indicator_insets,
[web_view_scroll_view_proxy_ scrollIndicatorInsets]));
// Arbitrary size.
const CGSize content_size = CGSizeMake(19, 19);
OCMStub([mock_underlying_scroll_view_ contentSize]).andReturn(content_size);
EXPECT_TRUE(CGSizeEqualToSize(content_size,
[web_view_scroll_view_proxy_ contentSize]));
// Arbitrary rect.
const CGRect frame = CGRectMake(2, 4, 5, 1);
OCMStub([mock_underlying_scroll_view_ frame]).andReturn(frame);
EXPECT_TRUE(CGRectEqualToRect(frame, [web_view_scroll_view_proxy_ frame]));
OCMExpect([mock_underlying_scroll_view_ isDecelerating]).andReturn(YES);
EXPECT_TRUE([web_view_scroll_view_proxy_ isDecelerating]);
OCMExpect([mock_underlying_scroll_view_ isDecelerating]).andReturn(NO);
EXPECT_FALSE([web_view_scroll_view_proxy_ isDecelerating]);
OCMExpect([mock_underlying_scroll_view_ isDragging]).andReturn(YES);
EXPECT_TRUE([web_view_scroll_view_proxy_ isDragging]);
OCMExpect([mock_underlying_scroll_view_ isDragging]).andReturn(NO);
EXPECT_FALSE([web_view_scroll_view_proxy_ isDragging]);
OCMExpect([mock_underlying_scroll_view_ isTracking]).andReturn(YES);
EXPECT_TRUE([web_view_scroll_view_proxy_ isTracking]);
OCMExpect([mock_underlying_scroll_view_ isTracking]).andReturn(NO);
EXPECT_FALSE([web_view_scroll_view_proxy_ isTracking]);
OCMExpect([mock_underlying_scroll_view_ scrollsToTop]).andReturn(YES);
EXPECT_TRUE([web_view_scroll_view_proxy_ scrollsToTop]);
OCMExpect([mock_underlying_scroll_view_ scrollsToTop]).andReturn(NO);
EXPECT_FALSE([web_view_scroll_view_proxy_ scrollsToTop]);
NSArray<__kindof UIView*>* subviews = [NSArray array];
OCMExpect([mock_underlying_scroll_view_ subviews]).andReturn(subviews);
EXPECT_EQ(subviews, [web_view_scroll_view_proxy_ subviews]);
OCMExpect([mock_underlying_scroll_view_ contentInsetAdjustmentBehavior])
.andReturn(UIScrollViewContentInsetAdjustmentAutomatic);
EXPECT_EQ(UIScrollViewContentInsetAdjustmentAutomatic,
[web_view_scroll_view_proxy_ contentInsetAdjustmentBehavior]);
OCMExpect([mock_underlying_scroll_view_ contentInsetAdjustmentBehavior])
.andReturn(UIScrollViewContentInsetAdjustmentNever);
EXPECT_EQ(UIScrollViewContentInsetAdjustmentNever,
[web_view_scroll_view_proxy_ contentInsetAdjustmentBehavior]);
OCMExpect([mock_underlying_scroll_view_ clipsToBounds]).andReturn(NO);
EXPECT_FALSE([web_view_scroll_view_proxy_ clipsToBounds]);
OCMExpect([mock_underlying_scroll_view_ clipsToBounds]).andReturn(YES);
EXPECT_TRUE([web_view_scroll_view_proxy_ clipsToBounds]);
}
// Tests that CRWWebViewScrollViewProxy returns the default values of
// UIScrollView's properties when there is no underlying UIScrollView.
TEST_F(CRWWebViewScrollViewProxyTest, ScrollViewAbsent) {
[web_view_scroll_view_proxy_ setScrollView:nil];
EXPECT_TRUE(CGPointEqualToPoint(CGPointZero,
[web_view_scroll_view_proxy_ contentOffset]));
EXPECT_TRUE(UIEdgeInsetsEqualToEdgeInsets(
UIEdgeInsetsZero, [web_view_scroll_view_proxy_ contentInset]));
EXPECT_TRUE(UIEdgeInsetsEqualToEdgeInsets(
UIEdgeInsetsZero, [web_view_scroll_view_proxy_ scrollIndicatorInsets]));
EXPECT_TRUE(
CGSizeEqualToSize(CGSizeZero, [web_view_scroll_view_proxy_ contentSize]));
EXPECT_TRUE(
CGRectEqualToRect(CGRectZero, [web_view_scroll_view_proxy_ frame]));
EXPECT_FALSE([web_view_scroll_view_proxy_ isDecelerating]);
EXPECT_FALSE([web_view_scroll_view_proxy_ isDragging]);
EXPECT_FALSE([web_view_scroll_view_proxy_ isTracking]);
EXPECT_TRUE([web_view_scroll_view_proxy_ scrollsToTop]);
EXPECT_EQ((NSUInteger)0, [web_view_scroll_view_proxy_ subviews].count);
EXPECT_EQ(UIScrollViewContentInsetAdjustmentAutomatic,
[web_view_scroll_view_proxy_ contentInsetAdjustmentBehavior]);
EXPECT_TRUE([web_view_scroll_view_proxy_ clipsToBounds]);
// Make sure setting the properties is fine too.
// Arbitrary point.
const CGPoint kPoint = CGPointMake(10, 10);
[web_view_scroll_view_proxy_ setContentOffset:kPoint];
// Arbitrary inset.
const UIEdgeInsets kContentInset = UIEdgeInsetsMake(10, 10, 10, 10);
[web_view_scroll_view_proxy_ setContentInset:kContentInset];
[web_view_scroll_view_proxy_ setScrollIndicatorInsets:kContentInset];
// Arbitrary size.
const CGSize kContentSize = CGSizeMake(19, 19);
[web_view_scroll_view_proxy_ setContentSize:kContentSize];
}
// Tests that CRWWebViewScrollViewProxy returns the correct property values when
// they are set while there isn't an underlying scroll view, then a new scroll
// view is set.
TEST_F(CRWWebViewScrollViewProxyTest, ScrollViewAbsentThenReset) {
[web_view_scroll_view_proxy_ setScrollView:nil];
UIScrollView* underlying_scroll_view = [[UIScrollView alloc] init];
OCMExpect([mock_underlying_scroll_view_ setClipsToBounds:YES]);
[web_view_scroll_view_proxy_ setClipsToBounds:YES];
OCMExpect([mock_underlying_scroll_view_
setContentInsetAdjustmentBehavior:
UIScrollViewContentInsetAdjustmentNever]);
[web_view_scroll_view_proxy_ setContentInsetAdjustmentBehavior:
UIScrollViewContentInsetAdjustmentNever];
[web_view_scroll_view_proxy_ setScrollView:underlying_scroll_view];
[web_view_scroll_view_proxy_ setScrollView:mock_underlying_scroll_view_];
EXPECT_OCMOCK_VERIFY(mock_underlying_scroll_view_);
}
// Tests that CRWWebViewScrollViewProxy returns the correct property values when
// they are set while there is an underlying scroll view, then a new scroll view
// is set.
TEST_F(CRWWebViewScrollViewProxyTest, ScrollViewPresentThenReset) {
[web_view_scroll_view_proxy_ setScrollView:nil];
UIScrollView* underlying_scroll_view = [[UIScrollView alloc] init];
[web_view_scroll_view_proxy_ setScrollView:underlying_scroll_view];
OCMExpect([mock_underlying_scroll_view_ setClipsToBounds:YES]);
[web_view_scroll_view_proxy_ setClipsToBounds:YES];
OCMExpect([mock_underlying_scroll_view_
setContentInsetAdjustmentBehavior:
UIScrollViewContentInsetAdjustmentNever]);
[web_view_scroll_view_proxy_ setContentInsetAdjustmentBehavior:
UIScrollViewContentInsetAdjustmentNever];
[web_view_scroll_view_proxy_ setScrollView:mock_underlying_scroll_view_];
EXPECT_OCMOCK_VERIFY(mock_underlying_scroll_view_);
}
// Tests releasing a scroll view when none is owned by the
// CRWWebViewScrollViewProxy.
TEST_F(CRWWebViewScrollViewProxyTest, ReleasingAScrollView) {
[web_view_scroll_view_proxy_ setScrollView:nil];
}
// Tests that CRWWebViewScrollViewProxy correctly delegates property setters to
// the underlying UIScrollView.
TEST_F(CRWWebViewScrollViewProxyTest, ScrollViewSetProperties) {
[web_view_scroll_view_proxy_ setScrollView:mock_underlying_scroll_view_];
OCMExpect([mock_underlying_scroll_view_
setContentInsetAdjustmentBehavior:
UIScrollViewContentInsetAdjustmentNever]);
[web_view_scroll_view_proxy_ setContentInsetAdjustmentBehavior:
UIScrollViewContentInsetAdjustmentNever];
EXPECT_OCMOCK_VERIFY(mock_underlying_scroll_view_);
}
// Tests that -setContentInsetAdjustmentBehavior: works even if it is called
// before setting the scroll view.
TEST_F(CRWWebViewScrollViewProxyTest,
SetContentInsetAdjustmentBehaviorBeforeSettingScrollView) {
OCMExpect([mock_underlying_scroll_view_
setContentInsetAdjustmentBehavior:
UIScrollViewContentInsetAdjustmentNever]);
[web_view_scroll_view_proxy_ setScrollView:nil];
[web_view_scroll_view_proxy_ setContentInsetAdjustmentBehavior:
UIScrollViewContentInsetAdjustmentNever];
[web_view_scroll_view_proxy_ setScrollView:mock_underlying_scroll_view_];
EXPECT_OCMOCK_VERIFY(mock_underlying_scroll_view_);
}
// Tests that -setClipsToBounds: works even if it is called before setting the
// scroll view.
TEST_F(CRWWebViewScrollViewProxyTest, SetClipsToBoundsBeforeSettingScrollView) {
OCMExpect([mock_underlying_scroll_view_ setClipsToBounds:YES]);
[web_view_scroll_view_proxy_ setScrollView:nil];
[web_view_scroll_view_proxy_ setClipsToBounds:YES];
[web_view_scroll_view_proxy_ setScrollView:mock_underlying_scroll_view_];
EXPECT_OCMOCK_VERIFY(mock_underlying_scroll_view_);
}
// Tests that frame changes are communicated to observers.
TEST_F(CRWWebViewScrollViewProxyTest, FrameDidChange) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(
web::features::kSmoothScrollingDefault);
UIScrollView* underlying_scroll_view =
[[UIScrollView alloc] initWithFrame:CGRectZero];
[web_view_scroll_view_proxy_ setScrollView:underlying_scroll_view];
id mock_delegate =
OCMProtocolMock(@protocol(CRWWebViewScrollViewProxyObserver));
[web_view_scroll_view_proxy_ addObserver:mock_delegate];
OCMExpect([mock_delegate
webViewScrollViewFrameDidChange:web_view_scroll_view_proxy_]);
underlying_scroll_view.frame = CGRectMake(1, 2, 3, 4);
EXPECT_OCMOCK_VERIFY(mock_delegate);
[web_view_scroll_view_proxy_ setScrollView:nil];
}
// Tests that contentInset changes are communicated to observers.
TEST_F(CRWWebViewScrollViewProxyTest, ContentInsetDidChange) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(
web::features::kSmoothScrollingDefault);
UIScrollView* underlying_scroll_view =
[[UIScrollView alloc] initWithFrame:CGRectZero];
[web_view_scroll_view_proxy_ setScrollView:underlying_scroll_view];
id mock_delegate =
OCMProtocolMock(@protocol(CRWWebViewScrollViewProxyObserver));
[web_view_scroll_view_proxy_ addObserver:mock_delegate];
OCMExpect([mock_delegate
webViewScrollViewDidResetContentInset:web_view_scroll_view_proxy_]);
underlying_scroll_view.contentInset = UIEdgeInsetsMake(0, 1, 2, 3);
EXPECT_OCMOCK_VERIFY(mock_delegate);
[web_view_scroll_view_proxy_ setScrollView:nil];
}
// Verifies that method calls to -asUIScrollView are simply forwarded to the
// underlying scroll view if the method is not implemented in
// CRWWebViewScrollViewProxy.
TEST_F(CRWWebViewScrollViewProxyTest, AsUIScrollViewWithUnderlyingScrollView) {
[web_view_scroll_view_proxy_ setScrollView:mock_underlying_scroll_view_];
// Verifies that a return value is properly propagated.
// -viewPrintFormatter is not implemented in CRWWebViewScrollViewProxy.
UIViewPrintFormatter* print_formatter_mock =
OCMClassMock([UIViewPrintFormatter class]);
OCMStub([mock_underlying_scroll_view_ viewPrintFormatter])
.andReturn(print_formatter_mock);
EXPECT_EQ(print_formatter_mock,
[[web_view_scroll_view_proxy_ asUIScrollView] viewPrintFormatter]);
// Verifies that a parameter is properly propagated.
// -drawRect: is not implemented in CRWWebViewScrollViewProxy.
CGRect rect = CGRectMake(0, 0, 1, 1);
OCMExpect([mock_underlying_scroll_view_ drawRect:rect]);
[[web_view_scroll_view_proxy_ asUIScrollView] drawRect:rect];
EXPECT_OCMOCK_VERIFY((id)mock_underlying_scroll_view_);
[web_view_scroll_view_proxy_ setScrollView:nil];
}
// Verifies that method calls to -asUIScrollView are no-op if the underlying
// scroll view is not set and the method is not implemented in
// CRWWebViewScrollViewProxy.
TEST_F(CRWWebViewScrollViewProxyTest,
AsUIScrollViewWithoutUnderlyingScrollView) {
[web_view_scroll_view_proxy_ setScrollView:nil];
// Any methods should return nil when the underlying scroll view is not set.
EXPECT_EQ(nil, [[web_view_scroll_view_proxy_ asUIScrollView]
restorationIdentifier]);
// It is expected that nothing happens. Just verifies that it doesn't crash.
CGRect rect = CGRectMake(0, 0, 1, 1);
[[web_view_scroll_view_proxy_ asUIScrollView] drawRect:rect];
}
// Verify that -[CRWWebViewScrollViewProxy isKindOfClass:] works as expected.
TEST_F(CRWWebViewScrollViewProxyTest, IsKindOfClass) {
// The proxy is a kind of its own class.
EXPECT_TRUE([web_view_scroll_view_proxy_
isKindOfClass:[CRWWebViewScrollViewProxy class]]);
// The proxy prentends itself to be a kind of UIScrollView.
EXPECT_TRUE([web_view_scroll_view_proxy_ isKindOfClass:[UIScrollView class]]);
// It should return YES for ancestor classes of UIScrollView.
EXPECT_TRUE([web_view_scroll_view_proxy_ isKindOfClass:[UIView class]]);
// Returns NO if none of above applies.
EXPECT_FALSE([web_view_scroll_view_proxy_ isKindOfClass:[NSString class]]);
}
// Verify that -[CRWWebViewScrollViewProxy respondsToSelector:] works as
// expected.
TEST_F(CRWWebViewScrollViewProxyTest, RespondsToSelector) {
// A method defined in CRWWebViewScrollViewProxy but not in UIScrollView.
EXPECT_TRUE(
[web_view_scroll_view_proxy_ respondsToSelector:@selector(addObserver:)]);
// A method defined in CRWWebViewScrollViewProxy and also in UIScrollView.
EXPECT_TRUE([web_view_scroll_view_proxy_
respondsToSelector:@selector(contentOffset)]);
// A method defined in UIScrollView but not in CRWWebViewScrollViewProxy.
EXPECT_TRUE([web_view_scroll_view_proxy_
respondsToSelector:@selector(indexDisplayMode)]);
// A method defined in UIView (a superclass of UIScrollView) but not in
// CRWWebViewScrollViewProxy.
EXPECT_TRUE([web_view_scroll_view_proxy_
respondsToSelector:@selector(viewPrintFormatter)]);
// A method defined in none of above.
EXPECT_FALSE([web_view_scroll_view_proxy_
respondsToSelector:@selector(containsString:)]);
}
// Tests delegate method forwarding to [web_view_scroll_view_proxy_
// asUIScrollView].delegate when:
// - [web_view_scroll_view_proxy_ asUIScrollView].delegate is not nil
// - CRWWebViewScrollViewDelegateProxy implements the method
//
// Expects that a method call to the delegate of the underlying scroll view is
// forwarded to [web_view_scroll_view_proxy_ asUIScrollView].delegate.
TEST_F(CRWWebViewScrollViewProxyTest,
ProxyDelegateMethodForwardingForImplementedMethod) {
UIScrollView* underlying_scroll_view = [[UIScrollView alloc] init];
[web_view_scroll_view_proxy_ setScrollView:underlying_scroll_view];
id<UIScrollViewDelegate> mock_proxy_delegate =
OCMProtocolMock(@protocol(UIScrollViewDelegate));
[web_view_scroll_view_proxy_ asUIScrollView].delegate = mock_proxy_delegate;
UIView* mock_view = OCMClassMock([UIView class]);
OCMExpect([mock_proxy_delegate
scrollViewWillBeginZooming:[web_view_scroll_view_proxy_ asUIScrollView]
withView:mock_view]);
EXPECT_TRUE([underlying_scroll_view.delegate
respondsToSelector:@selector(scrollViewWillBeginZooming:withView:)]);
[underlying_scroll_view.delegate
scrollViewWillBeginZooming:underlying_scroll_view
withView:mock_view];
EXPECT_OCMOCK_VERIFY(static_cast<id>(mock_proxy_delegate));
[web_view_scroll_view_proxy_ setScrollView:nil];
}
// Tests delegate method forwarding to [web_view_scroll_view_proxy_
// asUIScrollView].delegate when:
// - [web_view_scroll_view_proxy_ asUIScrollView].delegate is not nil
// - CRWWebViewScrollViewDelegateProxy does *not* implement the method
//
// Expects that a method call to the delegate of the underlying scroll view is
// forwarded to [web_view_scroll_view_proxy_ asUIScrollView].delegate.
TEST_F(CRWWebViewScrollViewProxyTest,
ProxyDelegateMethodForwardingForUnimplementedMethod) {
UIScrollView* underlying_scroll_view = [[UIScrollView alloc] init];
[web_view_scroll_view_proxy_ setScrollView:underlying_scroll_view];
id<UIScrollViewDelegate> mock_proxy_delegate =
OCMProtocolMock(@protocol(UIScrollViewDelegate));
[web_view_scroll_view_proxy_ asUIScrollView].delegate = mock_proxy_delegate;
UIView* mock_view = OCMClassMock([UIView class]);
OCMExpect([mock_proxy_delegate
viewForZoomingInScrollView:[web_view_scroll_view_proxy_
asUIScrollView]])
.andReturn(mock_view);
EXPECT_TRUE([underlying_scroll_view.delegate
respondsToSelector:@selector(viewForZoomingInScrollView:)]);
EXPECT_EQ(mock_view, [underlying_scroll_view.delegate
viewForZoomingInScrollView:underlying_scroll_view]);
EXPECT_OCMOCK_VERIFY(static_cast<id>(mock_proxy_delegate));
[web_view_scroll_view_proxy_ setScrollView:nil];
}
// Tests delegate method forwarding to [web_view_scroll_view_proxy_
// asUIScrollView].delegate when:
// - [web_view_scroll_view_proxy_ asUIScrollView].delegate is nil
// - CRWWebViewScrollViewDelegateProxy implements the method
//
// Expects that the delegate of the underlying scroll view responds to the
// method but does nothing.
TEST_F(CRWWebViewScrollViewProxyTest,
ProxyDelegateMethodForwardingForImplementedMethodWhenDelegateIsNil) {
UIScrollView* underlying_scroll_view = [[UIScrollView alloc] init];
[web_view_scroll_view_proxy_ setScrollView:underlying_scroll_view];
[web_view_scroll_view_proxy_ asUIScrollView].delegate = nil;
EXPECT_TRUE([underlying_scroll_view.delegate
respondsToSelector:@selector(scrollViewWillBeginZooming:withView:)]);
UIView* mock_view = OCMClassMock([UIView class]);
// Expects that nothing happens by calling this.
[underlying_scroll_view.delegate
scrollViewWillBeginZooming:underlying_scroll_view
withView:mock_view];
[web_view_scroll_view_proxy_ setScrollView:nil];
}
// Tests delegate method forwarding to [web_view_scroll_view_proxy_
// asUIScrollView].delegate when:
// - [web_view_scroll_view_proxy_ asUIScrollView].delegate is nil
// - CRWWebViewScrollViewDelegateProxy does *not* implement the method
//
// Expects that the delegate of the underlying scroll view does *not* respond to
// the method.
TEST_F(CRWWebViewScrollViewProxyTest,
ProxyDelegateMethodForwardingForUnimplementedMethodWhenDelegateIsNil) {
UIScrollView* underlying_scroll_view = [[UIScrollView alloc] init];
[web_view_scroll_view_proxy_ setScrollView:underlying_scroll_view];
[web_view_scroll_view_proxy_ asUIScrollView].delegate = nil;
EXPECT_FALSE([underlying_scroll_view.delegate
respondsToSelector:@selector(viewForZoomingInScrollView:)]);
[web_view_scroll_view_proxy_ setScrollView:nil];
}
// Verifies that adding a key-value observer to a CRWWebViewScrollViewProxy
// works as expected.
TEST_F(CRWWebViewScrollViewProxyTest, AddKVObserver) {
UIScrollView* underlying_scroll_view = [[UIScrollView alloc] init];
underlying_scroll_view.contentOffset = CGPointZero;
[web_view_scroll_view_proxy_ setScrollView:underlying_scroll_view];
// Add a key-value observer to a CRWWebViewScrollViewProxy.
NSObject* observer = OCMClassMock([NSObject class]);
int context = 0;
[web_view_scroll_view_proxy_
addObserver:observer
forKeyPath:@"contentOffset"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:&context];
// Setting `contentOffset` of the underlying scroll view should trigger a KVO
// notification. The `object` of the notification should be the
// CRWWebViewScrollViewProxy, not the underlying scroll view.
CGPoint new_offset = CGPointMake(10, 20);
NSDictionary<NSKeyValueChangeKey, id>* expected_change = @{
NSKeyValueChangeKindKey : @(NSKeyValueChangeSetting),
NSKeyValueChangeOldKey : @(CGPointZero),
NSKeyValueChangeNewKey : @(new_offset)
};
OCMExpect([observer observeValueForKeyPath:@"contentOffset"
ofObject:web_view_scroll_view_proxy_
change:expected_change
context:&context]);
underlying_scroll_view.contentOffset = new_offset;
EXPECT_OCMOCK_VERIFY(static_cast<id>(observer));
[web_view_scroll_view_proxy_ removeObserver:observer
forKeyPath:@"contentOffset"];
}
// Verifies that a key-value observer is kept after the underlying scroll view
// is set.
TEST_F(CRWWebViewScrollViewProxyTest,
KVObserversAreKeptAfterSettingUnderlyingScrollView) {
// Add a key-value observer to a CRWWebViewScrollViewProxy.
NSObject* observer = OCMClassMock([NSObject class]);
int context = 0;
[web_view_scroll_view_proxy_
addObserver:observer
forKeyPath:@"contentOffset"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:&context];
// Set the underlying scroll view.
UIScrollView* underlying_scroll_view = [[UIScrollView alloc] init];
underlying_scroll_view.contentOffset = CGPointZero;
[web_view_scroll_view_proxy_ setScrollView:underlying_scroll_view];
// KVO is inherited to the new underlying scroll view.
CGPoint new_offset = CGPointMake(10, 20);
NSDictionary<NSKeyValueChangeKey, id>* expected_change = @{
NSKeyValueChangeKindKey : @(NSKeyValueChangeSetting),
NSKeyValueChangeOldKey : @(CGPointZero),
NSKeyValueChangeNewKey : @(new_offset)
};
OCMExpect([observer observeValueForKeyPath:@"contentOffset"
ofObject:web_view_scroll_view_proxy_
change:expected_change
context:&context]);
underlying_scroll_view.contentOffset = new_offset;
EXPECT_OCMOCK_VERIFY(static_cast<id>(observer));
[web_view_scroll_view_proxy_ removeObserver:observer
forKeyPath:@"contentOffset"];
}
// Verifies that removing a key-value observer from a CRWWebViewScrollViewProxy
// works as expected.
TEST_F(CRWWebViewScrollViewProxyTest, RemoveKVObserver) {
UIScrollView* underlying_scroll_view = [[UIScrollView alloc] init];
underlying_scroll_view.contentOffset = CGPointZero;
[web_view_scroll_view_proxy_ setScrollView:underlying_scroll_view];
// Add and then remove a key-value observer.
NSObject* observer = OCMClassMock([NSObject class]);
int context = 0;
[web_view_scroll_view_proxy_
addObserver:observer
forKeyPath:@"contentOffset"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:&context];
[web_view_scroll_view_proxy_ removeObserver:observer
forKeyPath:@"contentOffset"];
// The observer should not be notified of a change after the removal.
CGPoint new_offset = CGPointMake(10, 20);
NSDictionary<NSKeyValueChangeKey, id>* expected_change = @{
NSKeyValueChangeKindKey : @(NSKeyValueChangeSetting),
NSKeyValueChangeOldKey : @(CGPointZero),
NSKeyValueChangeNewKey : @(new_offset)
};
[[static_cast<id>(observer) reject]
observeValueForKeyPath:@"contentOffset"
ofObject:web_view_scroll_view_proxy_
change:expected_change
context:&context];
underlying_scroll_view.contentOffset = new_offset;
EXPECT_OCMOCK_VERIFY(static_cast<id>(observer));
}
// When -addObserver:forKeyPath:options:context: is called multiple times with
// the same observer and key path, -removeObserver:forKeyPath: removes the last
// observation.
//
// This matches the (undocumented) behavior of the built-in KVO.
TEST_F(CRWWebViewScrollViewProxyTest, RemoveKVObserverRemovesLastObservation) {
UIScrollView* underlying_scroll_view = [[UIScrollView alloc] init];
underlying_scroll_view.contentOffset = CGPointZero;
[web_view_scroll_view_proxy_ setScrollView:underlying_scroll_view];
// Add an observer twice with `context1` and then with `context2`.
NSObject* observer = OCMClassMock([NSObject class]);
int context1 = 0;
int context2 = 0;
[web_view_scroll_view_proxy_
addObserver:observer
forKeyPath:@"contentOffset"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:&context1];
[web_view_scroll_view_proxy_
addObserver:observer
forKeyPath:@"contentOffset"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:&context2];
// Remove an observer once. This should remove the observation with
// `context2`.
[web_view_scroll_view_proxy_ removeObserver:observer
forKeyPath:@"contentOffset"];
// The observer should be notified of a change with `context1` but not with
// `context2`.
CGPoint new_offset = CGPointMake(10, 20);
NSDictionary<NSKeyValueChangeKey, id>* expected_change = @{
NSKeyValueChangeKindKey : @(NSKeyValueChangeSetting),
NSKeyValueChangeOldKey : @(CGPointZero),
NSKeyValueChangeNewKey : @(new_offset)
};
OCMExpect([observer observeValueForKeyPath:@"contentOffset"
ofObject:web_view_scroll_view_proxy_
change:expected_change
context:&context1]);
[[static_cast<id>(observer) reject]
observeValueForKeyPath:@"contentOffset"
ofObject:web_view_scroll_view_proxy_
change:expected_change
context:&context2];
underlying_scroll_view.contentOffset = new_offset;
EXPECT_OCMOCK_VERIFY(static_cast<id>(observer));
[web_view_scroll_view_proxy_ removeObserver:observer
forKeyPath:@"contentOffset"];
}
// Verifies that removing a key-value observer from a CRWWebViewScrollViewProxy
// works as expected when given a context.
TEST_F(CRWWebViewScrollViewProxyTest, RemoveKVObserverWithContext) {
UIScrollView* underlying_scroll_view = [[UIScrollView alloc] init];
underlying_scroll_view.contentOffset = CGPointZero;
[web_view_scroll_view_proxy_ setScrollView:underlying_scroll_view];
// Add an observer twice with `context1` and then with `context2`.
NSObject* observer = OCMClassMock([NSObject class]);
int context1 = 0;
int context2 = 0;
[web_view_scroll_view_proxy_
addObserver:observer
forKeyPath:@"contentOffset"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:&context1];
[web_view_scroll_view_proxy_
addObserver:observer
forKeyPath:@"contentOffset"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:&context2];
// Remove the observation with `context1`.
[web_view_scroll_view_proxy_ removeObserver:observer
forKeyPath:@"contentOffset"
context:&context1];
// The observer should be notified of a change with `context2` but not with
// `context1`.
CGPoint new_offset = CGPointMake(10, 20);
NSDictionary<NSKeyValueChangeKey, id>* expected_change = @{
NSKeyValueChangeKindKey : @(NSKeyValueChangeSetting),
NSKeyValueChangeOldKey : @(CGPointZero),
NSKeyValueChangeNewKey : @(new_offset)
};
OCMExpect([observer observeValueForKeyPath:@"contentOffset"
ofObject:web_view_scroll_view_proxy_
change:expected_change
context:&context2]);
[[static_cast<id>(observer) reject]
observeValueForKeyPath:@"contentOffset"
ofObject:web_view_scroll_view_proxy_
change:expected_change
context:&context1];
underlying_scroll_view.contentOffset = new_offset;
EXPECT_OCMOCK_VERIFY(static_cast<id>(observer));
[web_view_scroll_view_proxy_ removeObserver:observer
forKeyPath:@"contentOffset"
context:&context2];
}
// Verifies that it is safe to call -removeObserver:forKeyPath: against the
// proxy during -dealloc of the observer.
TEST_F(CRWWebViewScrollViewProxyTest,
RemoveKVObserverWhileDeallocatingObserver) {
// CRWTestObserver adds itself as a key-value observer of the proxy in its
// initializer, and removes itself as a observer during its -dealloc.
[[maybe_unused]] CRWTestObserver* observer =
[[CRWTestObserver alloc] initWithProxy:web_view_scroll_view_proxy_];
}
// Verifies that properties registered to `propertiesStore` are preserved if:
// - the setter is called when the underlying scroll view is not set
// - the getter is called after the underlying scroll view is still not set
TEST_F(CRWWebViewScrollViewProxyTest,
PreservePropertiesWhileUnderlyingScrollViewIsAbsent) {
// Recreate CRWWebViewScrollViewProxy with the updated feature flags.
web_view_scroll_view_proxy_ = [[CRWWebViewScrollViewProxy alloc] init];
[web_view_scroll_view_proxy_ setScrollView:nil];
// A preserved property with a primitive type.
[web_view_scroll_view_proxy_ asUIScrollView].directionalLockEnabled = YES;
EXPECT_TRUE(
[web_view_scroll_view_proxy_ asUIScrollView].directionalLockEnabled);
[web_view_scroll_view_proxy_ asUIScrollView].directionalLockEnabled = NO;
EXPECT_FALSE(
[web_view_scroll_view_proxy_ asUIScrollView].directionalLockEnabled);
// A preserved property with an object type.
[web_view_scroll_view_proxy_ asUIScrollView].tintColor = UIColor.redColor;
EXPECT_EQ(UIColor.redColor,
[web_view_scroll_view_proxy_ asUIScrollView].tintColor);
}
// Verifies that properties registered to `propertiesStore` are preserved if:
// - the setter is called when the underlying scroll view is not set
// - the getter is called after the underlying scroll view is set
TEST_F(CRWWebViewScrollViewProxyTest,
PreservePropertiesWhenUnderlyingScrollViewIsNewlyAssigned) {
// Recreate CRWWebViewScrollViewProxy with the updated feature flags.
web_view_scroll_view_proxy_ = [[CRWWebViewScrollViewProxy alloc] init];
[web_view_scroll_view_proxy_ setScrollView:nil];
[web_view_scroll_view_proxy_ asUIScrollView].directionalLockEnabled = YES;
[web_view_scroll_view_proxy_ asUIScrollView].tintColor = UIColor.redColor;
UIScrollView* underlying_scroll_view = [[UIScrollView alloc] init];
[web_view_scroll_view_proxy_ setScrollView:underlying_scroll_view];
// The properties are restored on the underlying scroll view.
EXPECT_TRUE(underlying_scroll_view.directionalLockEnabled);
EXPECT_EQ(UIColor.redColor, underlying_scroll_view.tintColor);
// The same property values are available via the scroll view proxy as well.
EXPECT_TRUE(
[web_view_scroll_view_proxy_ asUIScrollView].directionalLockEnabled);
EXPECT_EQ(UIColor.redColor,
[web_view_scroll_view_proxy_ asUIScrollView].tintColor);
[web_view_scroll_view_proxy_ setScrollView:nil];
}
// Verifies that properties registered to `propertiesStore` are preserved if:
// - the setter is called when the underlying scroll view is set
// - the getter is called after the underlying scroll view is reassigned
TEST_F(CRWWebViewScrollViewProxyTest,
PreservePropertiesWhenUnderlyingScrollViewIsReassigned) {
// Recreate CRWWebViewScrollViewProxy with the updated feature flags.
web_view_scroll_view_proxy_ = [[CRWWebViewScrollViewProxy alloc] init];
UIScrollView* underlying_scroll_view1 = [[UIScrollView alloc] init];
[web_view_scroll_view_proxy_ setScrollView:underlying_scroll_view1];
[web_view_scroll_view_proxy_ asUIScrollView].directionalLockEnabled = YES;
[web_view_scroll_view_proxy_ asUIScrollView].tintColor = UIColor.redColor;
UIScrollView* underlying_scroll_view2 = [[UIScrollView alloc] init];
[web_view_scroll_view_proxy_ setScrollView:underlying_scroll_view2];
// The properties are restored on the underlying scroll view.
EXPECT_TRUE(underlying_scroll_view2.directionalLockEnabled);
EXPECT_EQ(UIColor.redColor, underlying_scroll_view2.tintColor);
// The same property values are available via the scroll view proxy as well.
EXPECT_TRUE(
[web_view_scroll_view_proxy_ asUIScrollView].directionalLockEnabled);
EXPECT_EQ(UIColor.redColor,
[web_view_scroll_view_proxy_ asUIScrollView].tintColor);
[web_view_scroll_view_proxy_ setScrollView:nil];
}
// Verifies that the proxy uses the real implementation of a method defined in a
// category of UIScrollView while the underlying scroll view is not set.
TEST_F(CRWWebViewScrollViewProxyTest,
UIScrollViewCategoryWithoutUnderlyingScrollView) {
// Recreate CRWWebViewScrollViewProxy with the updated feature flags.
web_view_scroll_view_proxy_ = [[CRWWebViewScrollViewProxy alloc] init];
[web_view_scroll_view_proxy_ setScrollView:nil];
EXPECT_EQ(1,
[[web_view_scroll_view_proxy_ asUIScrollView] crw_categoryMethod]);
}
// Verifies that the proxy uses the real implementation of a method defined in a
// category of UIScrollView while the underlying scroll view is set.
TEST_F(CRWWebViewScrollViewProxyTest,
UIScrollViewCategoryWithUnderlyingScrollView) {
// Recreate CRWWebViewScrollViewProxy with the updated feature flags.
web_view_scroll_view_proxy_ = [[CRWWebViewScrollViewProxy alloc] init];
UIScrollView* underlying_scroll_view = [[UIScrollView alloc] init];
[web_view_scroll_view_proxy_ setScrollView:underlying_scroll_view];
EXPECT_EQ(1,
[[web_view_scroll_view_proxy_ asUIScrollView] crw_categoryMethod]);
}
// Verifies that the scroll view backgound color is not preserved between
// scroll views. Used to prevent regression of crbug.com/1078790.
TEST_F(CRWWebViewScrollViewProxyTest, DontPreserveBackgroundColor) {
// Recreate CRWWebViewScrollViewProxy with the updated feature flags.
web_view_scroll_view_proxy_ = [[CRWWebViewScrollViewProxy alloc] init];
// Set an underlying UIScrollView, and update its background color to red.
UIScrollView* underlying_scroll_view1 = [[UIScrollView alloc] init];
[web_view_scroll_view_proxy_ setScrollView:underlying_scroll_view1];
[web_view_scroll_view_proxy_ asUIScrollView].backgroundColor =
UIColor.redColor;
// Create a second UIScrollView and set its background color to black.
UIScrollView* underlying_scroll_view2 = [[UIScrollView alloc] init];
underlying_scroll_view2.backgroundColor = UIColor.blackColor;
[web_view_scroll_view_proxy_ setScrollView:underlying_scroll_view2];
// Verify that the second scroll view's background color remains black.
EXPECT_EQ(UIColor.blackColor, underlying_scroll_view2.backgroundColor);
}
} // namespace