blob: 7e5bcedb3afdd07ec72dad08faecd2761ee7595a [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 <UIKit/UIKit.h>
#include <memory>
#include "base/bind.h"
#include "base/callback.h"
#include "base/files/file_path.h"
#include "base/ios/block_types.h"
#include "base/memory/ref_counted.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "components/bookmarks/test/bookmark_test_helpers.h"
#include "components/history/core/browser/history_service.h"
#include "components/keyed_service/core/service_access_type.h"
#include "ios/chrome/browser/application_context.h"
#include "ios/chrome/browser/bookmarks/bookmark_model_factory.h"
#include "ios/chrome/browser/browser_state/test_chrome_browser_state.h"
#include "ios/chrome/browser/browser_state/test_chrome_browser_state_manager.h"
#import "ios/chrome/browser/chrome_url_util.h"
#include "ios/chrome/browser/history/history_service_factory.h"
#include "ios/chrome/browser/history/history_tab_helper.h"
#import "ios/chrome/browser/snapshots/snapshot_cache.h"
#import "ios/chrome/browser/snapshots/snapshot_cache_factory.h"
#import "ios/chrome/browser/snapshots/snapshot_tab_helper.h"
#import "ios/chrome/browser/tabs/legacy_tab_helper.h"
#import "ios/chrome/browser/tabs/tab.h"
#import "ios/chrome/browser/tabs/tab_helper_util.h"
#import "ios/chrome/browser/tabs/tab_model.h"
#import "ios/chrome/browser/tabs/tab_private.h"
#import "ios/chrome/browser/ui/open_in_controller.h"
#import "ios/chrome/browser/ui/open_in_controller_testing.h"
#import "ios/chrome/browser/web/tab_id_tab_helper.h"
#include "ios/chrome/test/block_cleanup_test.h"
#include "ios/chrome/test/ios_chrome_scoped_testing_chrome_browser_state_manager.h"
#include "ios/chrome/test/ios_chrome_scoped_testing_local_state.h"
#include "ios/public/provider/chrome/browser/test_chrome_browser_provider.h"
#import "ios/testing/ocmock_complex_type_helper.h"
#import "ios/web/navigation/navigation_manager_impl.h"
#include "ios/web/public/features.h"
#include "ios/web/public/navigation_item.h"
#import "ios/web/public/navigation_manager.h"
#include "ios/web/public/referrer.h"
#import "ios/web/public/test/fakes/fake_navigation_context.h"
#include "ios/web/public/test/test_web_thread_bundle.h"
#import "ios/web/test/fakes/crw_fake_back_forward_list.h"
#import "ios/web/web_state/navigation_context_impl.h"
#import "ios/web/web_state/ui/crw_web_controller.h"
#import "ios/web/web_state/web_state_impl.h"
#import "net/base/mac/url_conversions.h"
#include "net/http/http_response_headers.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/gtest_mac.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
// When C++ exceptions are disabled, the C++ library defines |try| and
// |catch| so as to allow exception-expecting C++ code to build properly when
// language support for exceptions is not present. These macros interfere
// with the use of |@try| and |@catch| in Objective-C files such as this one.
// Undefine these macros here, after everything has been #included, since
// there will be no C++ uses and only Objective-C uses from this point on.
#undef try
#undef catch
namespace {
const char kNewTabUrl[] = "chrome://newtab/";
const char kGoogleUserUrl[] = "http://google.com";
const char kGoogleRedirectUrl[] = "http://www.google.fr/";
static NSString* const kGoogleTitle = @"Google";
const char kOtherUserUrl[] = "http://other.com";
const char kOtherRedirectUrl[] = "http://other.fr/";
NSString* const kOtherTitle = @"Other";
const char kContentDispositionWithFilename[] =
"attachment; filename=\"suggested_filename.pdf\"";
const char kContentDispositionWithoutFilename[] =
"attachment; parameter=parameter_value";
const char kInvalidFilenameUrl[] = "http://www.hostname.com/";
const char kValidFilenameUrl[] = "http://www.hostname.com/filename.pdf";
} // namespace
@interface ArrayTabModel : TabModel {
@private
NSMutableArray* _tabsForTesting;
}
@end
@implementation ArrayTabModel
- (instancetype)init {
if ((self = [super init]))
_tabsForTesting = [[NSMutableArray alloc] initWithCapacity:1];
return self;
}
- (void)addTabForTesting:(Tab*)tab {
[_tabsForTesting addObject:tab];
}
- (NSUInteger)indexOfTab:(Tab*)tab {
return [_tabsForTesting indexOfObject:tab];
}
- (NSUInteger)count {
return [_tabsForTesting count];
}
- (void)closeTabAtIndex:(NSUInteger)index {
[_tabsForTesting removeObjectAtIndex:index];
}
@end
namespace {
// Observer of a QueryHistory request.
class HistoryQueryResultsObserver
: public base::RefCountedThreadSafe<HistoryQueryResultsObserver> {
public:
HistoryQueryResultsObserver(base::RunLoop* run_loop) : run_loop_(run_loop) {}
// Stores |results| and stops the current message loop.
void ProcessResults(history::QueryResults* results) {
results_.Swap(results);
run_loop_->QuitWhenIdle();
}
history::QueryResults* results() { return &results_; }
protected:
friend base::RefCountedThreadSafe<HistoryQueryResultsObserver>;
virtual ~HistoryQueryResultsObserver();
private:
history::QueryResults results_;
base::RunLoop* run_loop_;
};
HistoryQueryResultsObserver::~HistoryQueryResultsObserver() {}
// TabTest is parameterized on this enum to test both LegacyNavigationManager
// and WKBasedNavigationManager.
enum class NavigationManagerChoice {
LEGACY,
WK_BASED,
};
// TODO(crbug.com/620465): can a TestWebState be used instead of a WebStateImpl
// for those tests? This will require changing Tab to use a WebState instead of
// a WebStateImpl first though.
class TabTest : public BlockCleanupTest,
public ::testing::WithParamInterface<NavigationManagerChoice> {
public:
TabTest()
: scoped_browser_state_manager_(
std::make_unique<TestChromeBrowserStateManager>(base::FilePath())),
web_state_impl_(nullptr) {}
void SetUp() override {
BlockCleanupTest::SetUp();
if (GetParam() == NavigationManagerChoice::LEGACY) {
scoped_feature_list_.InitAndDisableFeature(
web::features::kSlimNavigationManager);
} else {
scoped_feature_list_.InitAndEnableFeature(
web::features::kSlimNavigationManager);
}
[[ChromeAppConstants sharedInstance]
setCallbackSchemeForTesting:@"chromium"];
// Set up the testing profiles.
TestChromeBrowserState::Builder test_cbs_builder;
test_cbs_builder.AddTestingFactory(
SnapshotCacheFactory::GetInstance(),
SnapshotCacheFactory::GetDefaultFactory());
chrome_browser_state_ = test_cbs_builder.Build();
chrome_browser_state_->CreateBookmarkModel(false);
bookmarks::test::WaitForBookmarkModelToLoad(
ios::BookmarkModelFactory::GetForBrowserState(
chrome_browser_state_.get()));
ASSERT_TRUE(chrome_browser_state_->CreateHistoryService(true));
ios::ChromeBrowserState* browser_state = chrome_browser_state_.get();
if (UseOffTheRecordBrowserState())
browser_state = browser_state->GetOffTheRecordChromeBrowserState();
mock_web_controller_ =
[OCMockObject niceMockForClass:[CRWWebController class]];
if (GetParam() == NavigationManagerChoice::WK_BASED) {
mock_web_view_ = [OCMockObject mockForClass:[WKWebView class]];
fake_wk_list_ = [[CRWFakeBackForwardList alloc] init];
OCMStub([mock_web_view_ backForwardList]).andReturn(fake_wk_list_);
OCMStub([mock_web_controller_ webViewNavigationProxy])
.andReturn(mock_web_view_);
}
OCMStub([mock_web_controller_ isViewAlive]).andReturn(YES);
web::WebState::CreateParams create_params(browser_state);
web_state_impl_ = std::make_unique<web::WebStateImpl>(create_params);
web_state_impl_->SetWebController(mock_web_controller_);
web_state_impl_->GetNavigationManagerImpl().InitializeSession();
web::WebStateImpl* web_state_impl = web_state_impl_.get();
[[[static_cast<OCMockObject*>(mock_web_controller_) stub]
andReturnValue:OCMOCK_VALUE(web_state_impl)] webStateImpl];
web_controller_view_ = [[UIView alloc] init];
[[[static_cast<OCMockObject*>(mock_web_controller_) stub]
andReturn:web_controller_view_] view];
TabIdTabHelper::CreateForWebState(web_state_impl_.get());
HistoryTabHelper::CreateForWebState(web_state_impl_.get());
LegacyTabHelper::CreateForWebState(web_state_impl_.get());
tab_ = LegacyTabHelper::GetTabForWebState(web_state_impl_.get());
web::NavigationManager::WebLoadParams load_params(
GURL("chrome://version/"));
[tab_ navigationManager]->LoadURLWithParams(load_params);
// There should be no entries in the history at this point.
history::QueryResults results;
QueryAllHistory(&results);
EXPECT_EQ(0UL, results.size());
}
void TearDown() override {
// Ensure that the Tab is destroyed before the autorelease pool is cleared.
web_state_impl_.reset();
BlockCleanupTest::TearDown();
}
void BrowseTo(const GURL& user_url,
const GURL& redirect_url,
NSString* title) {
DCHECK_EQ(tab_.webState, web_state_impl_.get());
std::unique_ptr<web::NavigationContextImpl> context1 =
web::NavigationContextImpl::CreateNavigationContext(
web_state_impl_.get(), user_url,
/*has_user_gesture=*/true,
ui::PageTransition::PAGE_TRANSITION_AUTO_BOOKMARK,
/*is_renderer_initiated=*/true);
web_state_impl_->OnNavigationStarted(context1.get());
web::Referrer empty_referrer;
web_state_impl_->GetNavigationManagerImpl().AddPendingItem(
redirect_url, empty_referrer, ui::PAGE_TRANSITION_CLIENT_REDIRECT,
web::NavigationInitiationType::RENDERER_INITIATED,
web::NavigationManager::UserAgentOverrideOption::INHERIT);
std::unique_ptr<web::NavigationContextImpl> context2 =
web::NavigationContextImpl::CreateNavigationContext(
web_state_impl_.get(), redirect_url,
/*has_user_gesture=*/true,
ui::PageTransition::PAGE_TRANSITION_AUTO_BOOKMARK,
/*is_renderer_initiated=*/true);
web_state_impl_->OnNavigationStarted(context2.get());
if (GetParam() == NavigationManagerChoice::WK_BASED) {
[fake_wk_list_
setCurrentURL:base::SysUTF8ToNSString(redirect_url.spec())];
}
web_state_impl_->GetNavigationManagerImpl().CommitPendingItem();
context2->SetHasCommitted(true);
web_state_impl_->UpdateHttpResponseHeaders(redirect_url);
web_state_impl_->OnNavigationFinished(context2.get());
web_state_impl_->SetIsLoading(true);
base::string16 new_title = base::SysNSStringToUTF16(title);
[tab_ navigationManager]->GetLastCommittedItem()->SetTitle(new_title);
web_state_impl_->OnTitleChanged();
web_state_impl_->SetIsLoading(false);
web_state_impl_->OnPageLoaded(redirect_url, true);
}
void BrowseToNewTab() {
DCHECK_EQ(tab_.webState, web_state_impl_.get());
const GURL url(kNewTabUrl);
// TODO(crbug.com/661992): This will not work with a mock CRWWebController.
// The only test that uses it is currently disabled.
web::NavigationManager::WebLoadParams params(url);
params.transition_type = ui::PAGE_TRANSITION_TYPED;
[tab_ navigationManager]->LoadURLWithParams(params);
web_state_impl_->SetIsLoading(true);
web_state_impl_->SetIsLoading(false);
web_state_impl_->OnPageLoaded(url, true);
web_state_impl_->OnTitleChanged();
}
void QueryAllHistory(history::QueryResults* results) {
base::CancelableTaskTracker tracker;
base::RunLoop run_loop;
scoped_refptr<HistoryQueryResultsObserver> observer(
new HistoryQueryResultsObserver(&run_loop));
history::HistoryService* history_service =
ios::HistoryServiceFactory::GetForBrowserState(
chrome_browser_state_.get(), ServiceAccessType::EXPLICIT_ACCESS);
history_service->QueryHistory(
base::string16(), history::QueryOptions(),
base::Bind(&HistoryQueryResultsObserver::ProcessResults, observer),
&tracker);
run_loop.Run();
results->Swap(observer->results());
}
void CheckHistoryResult(const history::URLResult& historyResult,
const GURL& expectedUrl,
NSString* expectedTitle) {
EXPECT_EQ(expectedUrl, historyResult.url());
EXPECT_EQ(base::SysNSStringToUTF16(expectedTitle), historyResult.title());
}
void CheckCurrentItem(const GURL& expectedUrl, NSString* expectedTitle) {
web::NavigationItem* item = [tab_ navigationManager]->GetVisibleItem();
EXPECT_EQ(expectedUrl, item->GetURL());
EXPECT_EQ(base::SysNSStringToUTF16(expectedTitle), item->GetTitle());
}
void CheckCurrentItem(const history::URLResult& historyResult) {
web::NavigationItem* item = [tab_ navigationManager]->GetVisibleItem();
CheckHistoryResult(historyResult, item->GetURL(),
base::SysUTF16ToNSString(item->GetTitle()));
}
#ifndef NDEBUG
// Method useful when debugging.
void LogHistoryQueryResult(const history::QueryResults* results) {
for (const auto& result : *results) {
NSLog(@"title = %@; url = %@", base::SysUTF16ToNSString(result.title()),
base::SysUTF8ToNSString(result.url().spec()));
}
}
#endif
virtual bool UseOffTheRecordBrowserState() const { return false; }
protected:
web::TestWebThreadBundle thread_bundle_;
IOSChromeScopedTestingChromeBrowserStateManager scoped_browser_state_manager_;
std::unique_ptr<TestChromeBrowserState> chrome_browser_state_;
std::unique_ptr<web::WebStateImpl> web_state_impl_;
__weak CRWWebController* mock_web_controller_;
WKWebView* mock_web_view_;
CRWFakeBackForwardList* fake_wk_list_;
UIView* web_controller_view_;
ArrayTabModel* tabModel_;
__weak Tab* tab_;
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_P(TabTest, AddToHistoryWithRedirect) {
BrowseTo(GURL(kGoogleUserUrl), GURL(kGoogleRedirectUrl), kGoogleTitle);
history::QueryResults results;
QueryAllHistory(&results);
EXPECT_EQ(1U, results.size());
CheckHistoryResult(results[0], GURL(kGoogleRedirectUrl), kGoogleTitle);
CheckCurrentItem(results[0]);
}
// TODO(crbug.com/378098): Disabled because forward/back is now implemented in
// CRWWebController, so this test cannot function with a mock CRWWebController.
// Rewrite and re-enable this test when it becomes a CRWWebController or
// NavigationManager test.
TEST_P(TabTest, DISABLED_BackAndForward) {
BrowseTo(GURL(kGoogleUserUrl), GURL(kGoogleRedirectUrl), kGoogleTitle);
BrowseTo(GURL(kOtherUserUrl), GURL(kOtherRedirectUrl), kOtherTitle);
history::QueryResults results;
QueryAllHistory(&results);
EXPECT_EQ(2U, results.size());
CheckHistoryResult(results[0], GURL(kOtherRedirectUrl), kOtherTitle);
CheckHistoryResult(results[1], GURL(kGoogleRedirectUrl), kGoogleTitle);
CheckCurrentItem(results[0]);
}
// TODO(crbug.com/378098): Disabled because navigation is no longer
// possible with a mock
// CRWWebController. Rewrite and re-enable this test when it becomes a
// CRWWebController test.
TEST_P(TabTest, DISABLED_NewTabInMiddleOfNavigation) {
BrowseTo(GURL(kGoogleUserUrl), GURL(kGoogleRedirectUrl), kGoogleTitle);
BrowseToNewTab();
BrowseTo(GURL(kOtherUserUrl), GURL(kOtherRedirectUrl), kOtherTitle);
// NOTE: NTP is not stored in the history.
history::QueryResults results;
QueryAllHistory(&results);
EXPECT_EQ(2U, results.size());
CheckHistoryResult(results[0], GURL(kOtherRedirectUrl), kOtherTitle);
CheckHistoryResult(results[1], GURL(kGoogleRedirectUrl), kGoogleTitle);
}
TEST_P(TabTest, GetSuggestedFilenameFromContentDisposition) {
// If possible, the filename should be generated from the content-disposition
// header.
GURL url(kValidFilenameUrl);
scoped_refptr<net::HttpResponseHeaders> headers =
new net::HttpResponseHeaders("HTTP 1.1 200 OK");
headers->AddHeader(base::StringPrintf("Content-Type: application/pdf"));
headers->AddHeader(base::StringPrintf("Content-Disposition: %s",
kContentDispositionWithFilename));
web_state_impl_->OnHttpResponseHeadersReceived(headers.get(), url);
BrowseTo(url, url, [NSString string]);
EXPECT_NSEQ(@"suggested_filename.pdf",
[[tab_ openInController] suggestedFilename]);
}
TEST_P(TabTest, GetSuggestedFilenameFromURL) {
// If the content-disposition header does not specify a filename, this should
// be extracted from the last component of the url.
GURL url(kValidFilenameUrl);
scoped_refptr<net::HttpResponseHeaders> headers =
new net::HttpResponseHeaders("HTTP 1.1 200 OK");
headers->AddHeader(base::StringPrintf("Content-Type: application/pdf"));
headers->AddHeader(base::StringPrintf("Content-Disposition: %s",
kContentDispositionWithoutFilename));
web_state_impl_->OnHttpResponseHeadersReceived(headers.get(), url);
BrowseTo(url, url, [NSString string]);
EXPECT_NSEQ(@"filename.pdf", [[tab_ openInController] suggestedFilename]);
}
TEST_P(TabTest, GetSuggestedFilenameFromDefaultName) {
// If the filename cannot be extracted from the content disposition or from
// the url, the default filename "Document.pdf" should be used.
GURL url(kInvalidFilenameUrl);
scoped_refptr<net::HttpResponseHeaders> headers =
new net::HttpResponseHeaders("HTTP 1.1 200 OK");
headers->AddHeader(base::StringPrintf("Content-Type: application/pdf"));
web_state_impl_->OnHttpResponseHeadersReceived(headers.get(), url);
BrowseTo(url, url, [NSString string]);
EXPECT_NSEQ(@"Document.pdf", [[tab_ openInController] suggestedFilename]);
}
TEST_P(TabTest, ClosingWebStateDoesNotRemoveSnapshot) {
id partialMock = OCMPartialMock(
SnapshotCacheFactory::GetForBrowserState(tab_.browserState));
SnapshotTabHelper::CreateForWebState(tab_.webState, tab_.tabId);
[[partialMock reject] removeImageWithSessionID:tab_.tabId];
// Use @try/@catch as -reject raises an exception.
@try {
web_state_impl_.reset();
EXPECT_OCMOCK_VERIFY(partialMock);
} @catch (NSException* exception) {
// The exception is raised when -removeImageWithSessionID: is invoked. As
// this should not happen, mark the test as failed.
GTEST_FAIL();
}
}
TEST_P(TabTest, CallingRemoveSnapshotRemovesSnapshot) {
id partialMock = OCMPartialMock(
SnapshotCacheFactory::GetForBrowserState(tab_.browserState));
SnapshotTabHelper::CreateForWebState(tab_.webState, tab_.tabId);
OCMExpect([partialMock removeImageWithSessionID:tab_.tabId]);
SnapshotTabHelper::FromWebState(tab_.webState)->RemoveSnapshot();
EXPECT_OCMOCK_VERIFY(partialMock);
}
INSTANTIATE_TEST_SUITE_P(ProgrammaticTabTest,
TabTest,
::testing::Values(NavigationManagerChoice::LEGACY,
NavigationManagerChoice::WK_BASED));
} // namespace