blob: 51872e9aa1b5ae4fe6aa81640b6c4117d852bf26 [file] [log] [blame]
// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import <QuartzCore/QuartzCore.h>
#include "base/strings/stringprintf.h"
#import "ios/chrome/browser/tabs/tab.h"
#import "ios/chrome/browser/tabs/tab_model.h"
#import "ios/chrome/browser/tabs/tab_model_observer.h"
#include "ios/chrome/browser/ui/rtl_geometry.h"
#import "ios/chrome/browser/ui/stack_view/card_set.h"
#import "ios/chrome/browser/ui/stack_view/card_stack_layout_manager.h"
#import "ios/chrome/browser/ui/stack_view/stack_card.h"
#import "ios/testing/ocmock_complex_type_helper.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/gtest_mac.h"
#include "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "third_party/ocmock/gtest_support.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
@interface MockTabModel : NSObject<NSFastEnumeration> {
@private
NSMutableArray* tabs_;
__weak id<TabModelObserver> observer_;
}
// Adds a new mock tab with the given properties.
- (void)addTabWithTitle:(NSString*)title location:(const GURL&)url;
// TabModel mocking.
- (NSUInteger)count;
- (BOOL)isOffTheRecord;
- (Tab*)currentTab;
- (NSUInteger)indexOfTab:(Tab*)tab;
- (Tab*)tabAtIndex:(NSUInteger)tabIndex;
- (void)addObserver:(id<TabModelObserver>)observer;
- (void)removeObserver:(id<TabModelObserver>)observer;
- (id<TabModelObserver>)observer;
@end
@interface CardSetTestTabMock : OCMockComplexTypeHelper
@end
@implementation CardSetTestTabMock
typedef const GURL& (^CardSetTestTabMock_url)(void);
- (const GURL&)url {
return static_cast<CardSetTestTabMock_url>([self blockForSelector:_cmd])();
}
@end
@implementation MockTabModel
- (id)init {
if ((self = [super init])) {
tabs_ = [[NSMutableArray alloc] init];
}
return self;
}
- (void)addTabWithTitle:(NSString*)title location:(const GURL&)url {
id tab = [[CardSetTestTabMock alloc]
initWithRepresentedObject:[OCMockObject mockForClass:[Tab class]]];
UIView* dummyView = [[UIView alloc] initWithFrame:CGRectZero];
static int sCounter = 0;
NSString* sessionID = [NSString stringWithFormat:@"%d", sCounter++];
BOOL no = NO;
[[[tab stub] andReturn:dummyView] view];
id block = [^{
return (const GURL&)url;
} copy];
[tab onSelector:@selector(url) callBlockExpectation:block];
[[tab expect] retrieveSnapshot:[OCMArg any]];
[[[tab stub] andReturn:nil] webController];
[[[tab stub] andReturn:nil] favicon];
[[[tab stub] andReturnValue:OCMOCK_VALUE(no)] canGoBack];
[[[tab stub] andReturnValue:OCMOCK_VALUE(no)] canGoForward];
[[[tab stub] andReturn:title] title];
[[[tab stub] andReturn:sessionID] tabId];
[tabs_ addObject:tab];
[observer_ tabModel:(TabModel*)self
didInsertTab:tab
atIndex:([tabs_ count] - 1)
inForeground:YES];
}
- (void)removeTabAtIndex:(NSUInteger)index {
id tab = [tabs_ objectAtIndex:index];
[tabs_ removeObjectAtIndex:index];
// A tab was removed at the given index.
[observer_ tabModel:(TabModel*)self didRemoveTab:tab atIndex:index];
}
- (NSUInteger)count {
return [tabs_ count];
}
- (BOOL)isOffTheRecord {
return NO;
}
- (Tab*)currentTab {
if ([tabs_ count])
return [tabs_ objectAtIndex:0];
return nil;
}
- (NSUInteger)indexOfTab:(Tab*)tab {
return [tabs_ indexOfObject:tab];
}
- (Tab*)tabAtIndex:(NSUInteger)tabIndex {
return [tabs_ objectAtIndex:tabIndex];
}
- (void)addObserver:(id<TabModelObserver>)observer {
observer_ = observer;
}
- (void)removeObserver:(id<TabModelObserver>)observer {
observer_ = nil;
}
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState*)state
objects:(__unsafe_unretained id*)stackbuf
count:(NSUInteger)len {
return [tabs_ countByEnumeratingWithState:state objects:stackbuf count:len];
}
- (id<TabModelObserver>)observer {
return observer_;
}
@end
#pragma mark -
namespace {
const CGFloat kViewportDimension = 100;
const CGFloat kCardDimension = 90;
CardStackLayoutManager* CreateMockCardStackLayoutManager() {
OCMockObject* stackModelMock =
[OCMockObject mockForClass:[CardStackLayoutManager class]];
BOOL no = NO;
[[[stackModelMock stub] andReturnValue:OCMOCK_VALUE(no)] layoutIsVertical];
NSUInteger lastStartStackCardIndex = 0;
[[[stackModelMock stub] andReturnValue:OCMOCK_VALUE(lastStartStackCardIndex)]
lastStartStackCardIndex];
NSArray* cards = @[];
[[[stackModelMock stub] andReturn:cards] cards];
return (CardStackLayoutManager*)stackModelMock;
}
class CardSetTest : public PlatformTest {
protected:
virtual void SetUpWithTabs(int nb_tabs) {
tab_model_ = [[MockTabModel alloc] init];
for (int i = 0; i < nb_tabs; ++i) {
std::string url = base::StringPrintf("http://%d.example.com", i);
[tab_model_ addTabWithTitle:@"NewTab" location:GURL(url)];
}
card_set_ = [[CardSet alloc] initWithModel:(TabModel*)tab_model_];
display_view_ = [[UIView alloc]
initWithFrame:CGRectMake(0, 0, kViewportDimension, kViewportDimension)];
// Do some initial configuration of the card set.
[card_set_ setDisplayView:display_view_];
[card_set_ setCardSize:CGSizeMake(kCardDimension, kCardDimension)];
[card_set_ setLayoutAxisPosition:(kViewportDimension / 2.0) isVertical:YES];
[card_set_ configureLayoutParametersWithMargin:10];
}
void SetUp() override { SetUpWithTabs(2); }
MockTabModel* tab_model_;
UIView* display_view_;
CardSet* card_set_;
};
TEST_F(CardSetTest, InitialLayoutState) {
NSArray* cards = [card_set_ cards];
EXPECT_EQ([tab_model_ count], [cards count]);
// Tabs should be on the trailing side.
EXPECT_EQ(CardCloseButtonSide::TRAILING, [card_set_ closeButtonSide]);
// At least one card should be visible after layout, and cards should have
// the right size and center.
[card_set_ fanOutCards];
BOOL has_visible_card = NO;
for (StackCard* card in cards) {
if ([card viewIsLive])
has_visible_card = YES;
EXPECT_FLOAT_EQ(kCardDimension, card.layout.size.width);
EXPECT_FLOAT_EQ(kCardDimension, card.layout.size.height);
EXPECT_FLOAT_EQ(kViewportDimension / 2.0,
CGRectGetMidX(LayoutRectGetRect(card.layout)));
}
EXPECT_TRUE(has_visible_card);
}
TEST_F(CardSetTest, HandleRotation) {
// Rotate the card set.
[card_set_ setLayoutAxisPosition:(kViewportDimension / 3.0) isVertical:NO];
// Tabs should now be on the leading side.
EXPECT_EQ(CardCloseButtonSide::LEADING, [card_set_ closeButtonSide]);
// And the centers should be on the new axis.
for (StackCard* card in [card_set_ cards]) {
EXPECT_FLOAT_EQ(kViewportDimension / 3.0,
CGRectGetMidY(LayoutRectGetRect(card.layout)));
}
}
TEST_F(CardSetTest, CurrentCard) {
NSUInteger current_tab_index =
[tab_model_ indexOfTab:[tab_model_ currentTab]];
NSArray* cards = [card_set_ cards];
NSUInteger current_card_index = [cards indexOfObject:[card_set_ currentCard]];
EXPECT_EQ(current_tab_index, current_card_index);
}
TEST_F(CardSetTest, ViewClearing) {
// Add the views.
[card_set_ fanOutCards];
ASSERT_GT([[display_view_ subviews] count], 0U);
[card_set_ setDisplayView:nil];
EXPECT_EQ(0U, [[display_view_ subviews] count]);
}
TEST_F(CardSetTest, NoticeTabAddition) {
NSArray* cards = [card_set_ cards];
NSUInteger initial_card_count = [cards count];
OCMockObject* observer =
[OCMockObject mockForProtocol:@protocol(CardSetObserver)];
[[observer expect] cardSet:OCMOCK_ANY didAddCard:OCMOCK_ANY];
[card_set_ setObserver:(id<CardSetObserver>)observer];
[tab_model_ addTabWithTitle:@"NewTab"
location:GURL("http://www.example.com")];
cards = [card_set_ cards];
EXPECT_EQ(initial_card_count + 1, [cards count]);
[card_set_ setObserver:nil];
EXPECT_OCMOCK_VERIFY(observer);
}
TEST_F(CardSetTest, NoticeTabRemoval) {
NSArray* cards = [card_set_ cards];
NSUInteger initial_card_count = [cards count];
StackCard* first_card = [cards objectAtIndex:0];
OCMockObject* observer =
[OCMockObject mockForProtocol:@protocol(CardSetObserver)];
[[observer expect] cardSet:OCMOCK_ANY willRemoveCard:OCMOCK_ANY atIndex:0];
[[observer expect] cardSet:OCMOCK_ANY didRemoveCard:OCMOCK_ANY atIndex:0];
[card_set_ setObserver:(id<CardSetObserver>)observer];
[tab_model_ removeTabAtIndex:0];
cards = [card_set_ cards];
EXPECT_EQ(initial_card_count - 1, [cards count]);
EXPECT_NE(first_card, [cards objectAtIndex:0]);
[card_set_ setObserver:nil];
EXPECT_OCMOCK_VERIFY(observer);
}
TEST_F(CardSetTest, Preloading) {
// Preloading when everything is visible should return NO.
[card_set_ fanOutCards];
EXPECT_FALSE([card_set_ preloadNextCard]);
// Add a bunch of cards to ensure stacking.
for (int i = 0; i < 20; ++i) {
[tab_model_ addTabWithTitle:@"NewTab"
location:GURL("http://www.example.com")];
}
[card_set_ fanOutCards];
NSUInteger loaded_cards = [[display_view_ subviews] count];
// Now preloading should return YES, and should have added one more card to
// the view.
EXPECT_TRUE([card_set_ preloadNextCard]);
EXPECT_EQ(loaded_cards + 1, [[display_view_ subviews] count]);
}
TEST_F(CardSetTest, stackIsFullyCollapsed) {
[card_set_ setStackModelForTesting:CreateMockCardStackLayoutManager()];
OCMockObject* stack_model_mock = (OCMockObject*)[card_set_ stackModel];
[[stack_model_mock expect] stackIsFullyCollapsed];
[card_set_ stackIsFullyCollapsed];
EXPECT_OCMOCK_VERIFY(stack_model_mock);
}
TEST_F(CardSetTest, stackIsFullyFannedOut) {
[card_set_ setStackModelForTesting:CreateMockCardStackLayoutManager()];
OCMockObject* stack_model_mock = (OCMockObject*)[card_set_ stackModel];
[[stack_model_mock expect] stackIsFullyFannedOut];
[card_set_ stackIsFullyFannedOut];
EXPECT_OCMOCK_VERIFY(stack_model_mock);
}
TEST_F(CardSetTest, stackIsFullyOverextended) {
[card_set_ setStackModelForTesting:CreateMockCardStackLayoutManager()];
OCMockObject* stack_model_mock = (OCMockObject*)[card_set_ stackModel];
[[stack_model_mock expect] stackIsFullyOverextended];
[card_set_ stackIsFullyOverextended];
EXPECT_OCMOCK_VERIFY(stack_model_mock);
}
TEST_F(CardSetTest, isCardInStartStaggerRegion) {
[card_set_ fanOutCards];
NSArray* cards = [card_set_ cards];
// First card should not be collapsed into start stagger region when stack is
// fanned out from the first card.
StackCard* first_card = [cards objectAtIndex:0];
EXPECT_FALSE([card_set_ isCardInStartStaggerRegion:first_card]);
// Add a bunch of cards to ensure stacking, and fan them out so that there
// is a start stack.
for (int i = 0; i < 20; ++i) {
[tab_model_ addTabWithTitle:@"NewTab"
location:GURL("http://www.example.com")];
}
[card_set_ fanOutCardsWithStartIndex:10];
// First card should now be collapsed into start stagger region.
EXPECT_TRUE([card_set_ isCardInStartStaggerRegion:first_card]);
}
TEST_F(CardSetTest, isCardInEndStaggerRegion) {
[card_set_ fanOutCards];
NSArray* cards = [card_set_ cards];
// Add a bunch of cards to ensure stacking, and fan them out so that there
// is an end stack.
for (int i = 0; i < 20; ++i) {
[tab_model_ addTabWithTitle:@"NewTab"
location:GURL("http://www.example.com")];
}
StackCard* last_card = [cards objectAtIndex:[cards count] - 1];
[card_set_ fanOutCards];
// Last card should be collapsed into end region.
EXPECT_TRUE([card_set_ isCardInEndStaggerRegion:last_card]);
// Fan out cards from the last card.
[card_set_ fanOutCardsWithStartIndex:[cards count] - 1];
// Last card should not be collapsed into end region.
EXPECT_FALSE([card_set_ isCardInEndStaggerRegion:last_card]);
}
TEST_F(CardSetTest, setTabModel) {
SetUpWithTabs(0);
[card_set_ setTabModel:nil];
EXPECT_TRUE([tab_model_ observer] == nil);
[card_set_ setTabModel:static_cast<id>(tab_model_)];
EXPECT_NSEQ(card_set_, static_cast<id>([tab_model_ observer]));
}
} // namespace