// Copyright 2023 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/chrome/browser/bring_android_tabs/model/bring_android_tabs_to_ios_service.h"
#import <numeric>
#import "base/i18n/number_formatting.h"
#import "base/strings/utf_string_conversions.h"
#import "base/test/metrics/histogram_tester.h"
#import "components/prefs/pref_registry_simple.h"
#import "components/prefs/pref_service.h"
#import "components/prefs/testing_pref_service.h"
#import "components/segmentation_platform/embedder/default_model/device_switcher_model.h"
#import "components/segmentation_platform/embedder/default_model/device_switcher_result_dispatcher.h"
#import "components/segmentation_platform/public/field_trial_register.h"
#import "components/segmentation_platform/public/result.h"
#import "components/segmentation_platform/public/segmentation_platform_service.h"
#import "components/segmentation_platform/public/testing/mock_segmentation_platform_service.h"
#import "components/sessions/core/serialized_navigation_entry_test_helper.h"
#import "components/sessions/core/session_id.h"
#import "components/sessions/core/session_types.h"
#import "components/sync/base/user_selectable_type.h"
#import "components/sync/service/sync_service.h"
#import "components/sync/service/sync_user_settings.h"
#import "components/sync/test/test_sync_service.h"
#import "components/sync_device_info/device_info.h"
#import "components/sync_device_info/fake_device_info_tracker.h"
#import "components/sync_sessions/open_tabs_ui_delegate.h"
#import "components/sync_sessions/session_sync_service.h"
#import "components/sync_sessions/session_sync_test_helper.h"
#import "components/sync_sessions/synced_session.h"
#import "ios/chrome/browser/bring_android_tabs/model/metrics.h"
#import "ios/chrome/browser/first_run/model/first_run.h"
#import "ios/chrome/browser/segmentation_platform/model/segmentation_platform_config.h"
#import "ios/chrome/browser/shared/model/browser/test/test_browser.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/sync/model/session_sync_service_factory.h"
#import "ios/chrome/browser/url_loading/model/fake_url_loading_browser_agent.h"
#import "ios/chrome/browser/url_loading/model/url_loading_notifier_browser_agent.h"
#import "ios/web/public/test/web_task_environment.h"
#import "testing/platform_test.h"
#import "ui/base/device_form_factor.h"
#import "url/gurl.h"
namespace {
// Number of test foreign sessions from a phone.
const size_t kPhoneSessionCount = 2;
// Number of test foreign sessions from a tablet.
const size_t kTabletSessionCount = 1;
// Maximum number of tabs that should be imported.
const int kMaxNumberOfTabs = 20;
// Maximum number of tabs that should be instant loaded.
const int kMaxNumberOfInstantLoadedTabs = 6;
// Amount of duplicate foreign tabs per session to create when needed.
const int kNumberOfDuplicatesToCreatePerSession = 1;
// Test URL.
const std::string kTestURLPrefix = "https://url";
std::string GetTestURLSpec(int index) {
return kTestURLPrefix + base::UTF16ToUTF8(base::FormatNumber(index)) + "/";
}
// Test title.
const std::u16string kTestTitlePrefix = u"title";
std::u16string GetTestTitle(int index) {
return kTestTitlePrefix + base::FormatNumber(index);
}
} // namespace
namespace bring_android_tabs {
// Fake DeviceSwitcherResultDispatcher that takes a `classification_label` as
// input. DeviceSwitcherResultDispatcher is a dependency of
// BringAndroidTabsToIOSService.
class FakeDeviceSwitcherResultDispatcher
: public segmentation_platform::DeviceSwitcherResultDispatcher {
public:
FakeDeviceSwitcherResultDispatcher(
segmentation_platform::SegmentationPlatformService* segmentation_service,
syncer::DeviceInfoTracker* device_info_tracker,
PrefService* prefs,
segmentation_platform::FieldTrialRegister* field_trial_register,
const char* classification_label)
: DeviceSwitcherResultDispatcher(segmentation_service,
device_info_tracker,
prefs,
field_trial_register) {
classification_label_ = classification_label;
}
// Returns a classification result with a successful PredictionStatus and
// label `classification_label`.
segmentation_platform::ClassificationResult GetCachedClassificationResult()
override {
segmentation_platform::ClassificationResult classification_result =
segmentation_platform::ClassificationResult(
segmentation_platform::PredictionStatus::kSucceeded);
classification_result.ordered_labels = {classification_label_};
return classification_result;
}
private:
const char* classification_label_;
};
// Mock SessionSyncService used to override the call to GetOpenTabsUIDelegate().
// SessionSyncService is a dependency of BringAndroidTabsToIOSService.
class MockSessionSyncService : public sync_sessions::SessionSyncService {
public:
MOCK_METHOD(sync_sessions::OpenTabsUIDelegate*,
GetOpenTabsUIDelegate,
(),
(override));
MOCK_METHOD(syncer::GlobalIdMapper*, GetGlobalIdMapper, (), (const));
MOCK_METHOD(base::CallbackListSubscription,
SubscribeToForeignSessionsChanged,
(const base::RepeatingClosure&));
MOCK_METHOD(base::WeakPtr<syncer::DataTypeControllerDelegate>,
GetControllerDelegate,
());
};
// Mock OpenTabsUIDelegate that takes the time the SyncedSession was last
// modified as input and creates a fake open tab. OpenTabsUIDelegate is a
// dependency of SessionSyncService.
class MockOpenTabsUIDelegate : public sync_sessions::OpenTabsUIDelegate {
public:
MockOpenTabsUIDelegate(int tab_per_session, base::Time modified_time)
: sync_sessions::OpenTabsUIDelegate(),
tab_per_session_(tab_per_session),
modified_time_(modified_time) {
tab_index_ = 0;
create_duplicates_ = false;
}
MOCK_METHOD(bool,
GetAllForeignSessions,
((std::vector<raw_ptr<const sync_sessions::SyncedSession,
VectorExperimental>>*)),
(override));
MOCK_METHOD(bool,
GetForeignSessionTabs,
(const std::string&, std::vector<const sessions::SessionTab*>*),
(override));
MOCK_METHOD(bool,
GetForeignTab,
(const std::string&, SessionID, const sessions::SessionTab**));
MOCK_METHOD(void, DeleteForeignSession, (const std::string&));
MOCK_METHOD(std::vector<const sessions::SessionWindow*>,
GetForeignSession,
(const std::string&));
MOCK_METHOD(bool, GetLocalSession, (const sync_sessions::SyncedSession**));
// Returns a fake tab with timestamp `modified_time_`.
sessions::SessionTab* Tab(int index) {
sessions::SerializedNavigationEntry entry = sessions::
SerializedNavigationEntryTestHelper::CreateNavigationForTest();
entry.set_virtual_url(GURL(GetTestURLSpec(index)));
entry.set_title(GetTestTitle(index));
sessions::SessionTab* tab = new sessions::SessionTab();
SessionID session_id = SessionID::FromSerializedValue(100 * index + 1);
tab->window_id = session_id;
tab->tab_id = session_id;
tab->tab_visual_index = 100;
tab->current_navigation_index = 1000;
tab->pinned = false;
tab->extension_app_id = "fake";
tab->user_agent_override.ua_string_override = "fake";
tab->timestamp = modified_time_;
tab->navigations = std::vector<sessions::SerializedNavigationEntry>{entry};
tab->session_storage_persistent_id = "fake";
return tab;
}
// Returns a fake session with modified time `modified_time_` and form factor
// type `device_form_factor`.
sync_sessions::SyncedSession* Session(
sync_pb::SyncEnums::DeviceType device_type,
syncer::DeviceInfo::FormFactor device_form_factor) {
sync_sessions::SyncedSession* session = new sync_sessions::SyncedSession();
session->SetDeviceTypeAndFormFactor(device_type, device_form_factor);
session->SetModifiedTime(modified_time_);
return session;
}
// Mocks foreign sessions for GetAllForeignSessions() and
// GetForeignSessionTabs(). Three sessions will be created, with two phone
// sessions and one tablet session. There will be `tab_per_session_` tabs in
// each session.
void MockForeignSessions() {
ON_CALL(*this, GetAllForeignSessions)
.WillByDefault(
[this](std::vector<raw_ptr<const sync_sessions::SyncedSession,
VectorExperimental>>* sessions) {
for (size_t i = 0; i < kPhoneSessionCount; i++) {
sessions->push_back(
Session(sync_pb::SyncEnums_DeviceType_TYPE_PHONE,
syncer::DeviceInfo::FormFactor::kPhone));
}
for (size_t i = 0; i < kTabletSessionCount; i++) {
sessions->push_back(
Session(sync_pb::SyncEnums_DeviceType_TYPE_TABLET,
syncer::DeviceInfo::FormFactor::kTablet));
}
return true;
});
ON_CALL(*this, GetForeignSessionTabs)
.WillByDefault([this](const std::string& tag,
std::vector<const sessions::SessionTab*>* tabs) {
// Use tab_index_ to not have duplicate tab titles/URLs.
for (int i = 0; i < tab_per_session_; i++) {
tabs->push_back(Tab(tab_index_++));
}
if (create_duplicates_) {
for (int i = 0; i < kNumberOfDuplicatesToCreatePerSession; i++) {
tabs->push_back(Tab(i));
}
}
return true;
});
}
void SetCreateDuplicates(bool create_duplicates) {
create_duplicates_ = create_duplicates;
}
private:
int tab_per_session_;
int tab_index_;
bool create_duplicates_;
base::Time modified_time_;
};
} // namespace bring_android_tabs
// Test fixture for BringAndroidTabsToIOSService.
class BringAndroidTabsToIOSServiceTest : public PlatformTest {
protected:
BringAndroidTabsToIOSServiceTest() : PlatformTest() {
FirstRun::RemoveSentinel();
FirstRun::ClearStateForTesting();
browser_state_ = TestChromeBrowserState::Builder().Build();
browser_ = std::make_unique<TestBrowser>(browser_state_.get());
device_info_tracker_ = std::make_unique<syncer::FakeDeviceInfoTracker>();
test_sync_service_ = std::make_unique<syncer::TestSyncService>();
prefs_ = std::make_unique<TestingPrefServiceSimple>();
segmentation_platform::DeviceSwitcherResultDispatcher::RegisterProfilePrefs(
prefs_->registry());
prefs_->registry()->RegisterBooleanPref(
prefs::kIosBringAndroidTabsPromptDisplayed, false);
UrlLoadingNotifierBrowserAgent::CreateForBrowser(browser_.get());
FakeUrlLoadingBrowserAgent::InjectForBrowser(browser_.get());
}
// Helper method that creates a fake OpenTabsUIDelegate for testing purpose.
void SetUpOpenTabsUIDelegate(int tab_per_session, bool tabs_recently_active) {
// Create the fake tab.
base::Time session_time = tabs_recently_active
? base::Time::Now()
: base::Time::Now() - base::Days(14);
open_ui_delegate_ =
std::make_unique<bring_android_tabs::MockOpenTabsUIDelegate>(
tab_per_session, session_time);
open_ui_delegate_->MockForeignSessions();
}
// Helper method that creates a fake OpenTabsUIDelegate with duplicate foreign
// tabs for testing purposes.
void SetUpOpenTabsUIDelegateWithDuplicates(int tab_per_session,
bool tabs_recently_active) {
base::Time session_time = tabs_recently_active
? base::Time::Now()
: base::Time::Now() - base::Days(14);
open_ui_delegate_ =
std::make_unique<bring_android_tabs::MockOpenTabsUIDelegate>(
tab_per_session, session_time);
open_ui_delegate_->SetCreateDuplicates(true);
open_ui_delegate_->MockForeignSessions();
}
// Helper method that creates an instance of BringAndroidTabsToIOSService and
// loads the user's tabs. Also records that the prompt is displayed if at
// least one tab is loaded.
void SetUpBringAndroidTabsServiceAndLoadTabs(bool is_android_switcher) {
// Create BringAndroidTabsToIOSService dependencies. These dependencies are
// only used in `LoadTabs()`, therefore they can be scoped within this
// method.
auto session_sync_service =
std::make_unique<bring_android_tabs::MockSessionSyncService>();
ON_CALL(*session_sync_service, GetOpenTabsUIDelegate)
.WillByDefault(testing::Return(open_ui_delegate_.get()));
const char* classification_label =
is_android_switcher
? segmentation_platform::DeviceSwitcherModel::kAndroidPhoneLabel
: segmentation_platform::DeviceSwitcherModel::kIosPhoneChromeLabel;
segmentation_platform::IOSFieldTrialRegisterImpl field_trial_register_ =
segmentation_platform::IOSFieldTrialRegisterImpl();
bring_android_tabs::FakeDeviceSwitcherResultDispatcher dispatcher(
&segmentation_platform_service_, device_info_tracker_.get(),
prefs_.get(), &field_trial_register_, classification_label);
// Create the BringAndroidTabsToIOSService and load tabs.
bring_android_tabs_service_ =
std::make_unique<BringAndroidTabsToIOSService>(
&dispatcher, test_sync_service_.get(), session_sync_service.get(),
prefs_.get());
bring_android_tabs_service_->LoadTabs();
// Record that the prompt has displayed.
if (bring_android_tabs_service_->GetNumberOfAndroidTabs() > 0) {
bring_android_tabs_service_->OnBringAndroidTabsPromptDisplayed();
}
}
// Returns the fake Url Loader for testing purpose.
FakeUrlLoadingBrowserAgent* GetTestUrlLoader() {
return FakeUrlLoadingBrowserAgent::FromUrlLoadingBrowserAgent(
UrlLoadingBrowserAgent::FromBrowser(browser_.get()));
}
// Sets the data types that are synced.
void SetSelectedTypes(syncer::UserSelectableTypeSet types) {
test_sync_service_->GetUserSettings()->SetSelectedTypes(
/*sync_everything=*/false,
/*types=*/types);
}
// Helper method that checks if `prompt_attempt_status` is recorded in the
// histogram with name `kPromptAttemptStatusHistogramName`. The metrics
// recording in BringAndroidTabsToIOSServiceTest is only done for iPhone
// users.
void ExpectHistogram(
bring_android_tabs::PromptAttemptStatus prompt_attempt_status) {
if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_PHONE) {
histogram_tester_.ExpectBucketCount(
bring_android_tabs::kPromptAttemptStatusHistogramName,
static_cast<base::HistogramBase::Sample>(prompt_attempt_status), 1);
}
}
BringAndroidTabsToIOSService* bring_android_tabs_to_ios_service() {
CHECK(bring_android_tabs_service_);
return bring_android_tabs_service_.get();
}
private:
web::WebTaskEnvironment task_environment_;
std::unique_ptr<TestChromeBrowserState> browser_state_;
std::unique_ptr<TestBrowser> browser_;
std::unique_ptr<bring_android_tabs::MockOpenTabsUIDelegate> open_ui_delegate_;
std::unique_ptr<BringAndroidTabsToIOSService> bring_android_tabs_service_;
// Service dependencies.
testing::NiceMock<segmentation_platform::MockSegmentationPlatformService>
segmentation_platform_service_;
std::unique_ptr<syncer::TestSyncService> test_sync_service_;
std::unique_ptr<syncer::FakeDeviceInfoTracker> device_info_tracker_;
std::unique_ptr<TestingPrefServiceSimple> prefs_;
base::HistogramTester histogram_tester_;
};
// Tests that no tabs are loaded when the user has not synced their tabs.
TEST_F(BringAndroidTabsToIOSServiceTest, UserNotSynced) {
SetUpOpenTabsUIDelegate(/*tab_per_session=*/2, /*tabs_recently_active=*/true);
// Set something other than `kTabs` as the selected type.
SetSelectedTypes({syncer::UserSelectableType::kPasswords});
SetUpBringAndroidTabsServiceAndLoadTabs(/*is_android_switcher=*/true);
EXPECT_EQ(bring_android_tabs_to_ios_service()->GetNumberOfAndroidTabs(), 0u);
ExpectHistogram(bring_android_tabs::PromptAttemptStatus::kTabSyncDisabled);
}
// Tests that no tabs are loaded when the user is not an android switcher.
TEST_F(BringAndroidTabsToIOSServiceTest, UserNotAndroidSwitcher) {
SetUpOpenTabsUIDelegate(/*tab_per_session=*/2, /*tabs_recently_active=*/true);
SetUpBringAndroidTabsServiceAndLoadTabs(/*is_android_switcher=*/false);
EXPECT_EQ(bring_android_tabs_to_ios_service()->GetNumberOfAndroidTabs(), 0u);
}
// Tests that no tabs are loaded when the user's open tabs were not recently
// opened.
TEST_F(BringAndroidTabsToIOSServiceTest, UserDoesNotHaveRecentlyOpenedTabs) {
SetUpOpenTabsUIDelegate(/*tab_per_session=*/2,
/*tabs_recently_active=*/false);
SetUpBringAndroidTabsServiceAndLoadTabs(/*is_android_switcher=*/true);
EXPECT_EQ(bring_android_tabs_to_ios_service()->GetNumberOfAndroidTabs(), 0u);
ExpectHistogram(bring_android_tabs::PromptAttemptStatus::kNoActiveTabs);
}
// Tests that no tabs are loaded when the user has already seen the prompt in a
// previous session.
TEST_F(BringAndroidTabsToIOSServiceTest, UserHasSeenPrompt) {
if (ui::GetDeviceFormFactor() != ui::DEVICE_FORM_FACTOR_PHONE) {
GTEST_SKIP() << "Feature unsupported on iPad";
}
size_t tab_per_session = 2;
SetUpOpenTabsUIDelegate(/*tab_per_session=*/tab_per_session,
/*tabs_recently_active=*/true);
// Load tabs and record prompt is displayed.
SetUpBringAndroidTabsServiceAndLoadTabs(/*is_android_switcher=*/true);
EXPECT_EQ(bring_android_tabs_to_ios_service()->GetNumberOfAndroidTabs(),
tab_per_session * kPhoneSessionCount);
// Simulate restart.
SetUpBringAndroidTabsServiceAndLoadTabs(/*is_android_switcher=*/true);
EXPECT_EQ(bring_android_tabs_to_ios_service()->GetNumberOfAndroidTabs(), 0u);
ExpectHistogram(
bring_android_tabs::PromptAttemptStatus::kPromptShownAndDismissed);
}
// Tests that the user's tab is loaded when they meet all criteria to be shown
// the Bring Android Tabs prompt. The prompt should only be shown on iPhone.
TEST_F(BringAndroidTabsToIOSServiceTest, UserMeetsAllCriteria) {
int tab_per_session = 2;
SetUpOpenTabsUIDelegate(/*tab_per_session=*/tab_per_session,
/*tabs_recently_active=*/true);
size_t expected_number_of_tabs =
ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_PHONE
? tab_per_session * kPhoneSessionCount
: 0u;
SetUpBringAndroidTabsServiceAndLoadTabs(/*is_android_switcher=*/true);
EXPECT_EQ(bring_android_tabs_to_ios_service()->GetNumberOfAndroidTabs(),
expected_number_of_tabs);
ExpectHistogram(bring_android_tabs::PromptAttemptStatus::kSuccess);
}
// Tests that the first few tabs are instant loaded.
TEST_F(BringAndroidTabsToIOSServiceTest, InstantLoadFirstFewTabs) {
if (ui::GetDeviceFormFactor() != ui::DEVICE_FORM_FACTOR_PHONE) {
GTEST_SKIP() << "Feature unsupported on iPad";
}
int tab_per_session = kMaxNumberOfInstantLoadedTabs / kPhoneSessionCount;
SetUpOpenTabsUIDelegate(tab_per_session,
/*tabs_recently_active=*/true);
SetUpBringAndroidTabsServiceAndLoadTabs(/*is_android_switcher=*/true);
ASSERT_EQ(bring_android_tabs_to_ios_service()->GetNumberOfAndroidTabs(),
static_cast<size_t>(kMaxNumberOfInstantLoadedTabs));
ExpectHistogram(bring_android_tabs::PromptAttemptStatus::kSuccess);
// Verify that the tabs are instant loaded.
FakeUrlLoadingBrowserAgent* url_loader = GetTestUrlLoader();
bring_android_tabs_to_ios_service()->OpenAllTabs(url_loader);
EXPECT_EQ(kMaxNumberOfInstantLoadedTabs, url_loader->load_new_tab_call_count);
EXPECT_EQ(GetTestURLSpec(kMaxNumberOfInstantLoadedTabs - 1),
url_loader->last_params.web_params.url.spec());
EXPECT_EQ(GetTestTitle(kMaxNumberOfInstantLoadedTabs - 1),
url_loader->last_params.placeholder_title);
EXPECT_TRUE(url_loader->last_params.instant_load);
}
// Tests that if there are more tabs than fitting in the device screen but less
// than maximum tabs that should be loaded, all tabs are opened, but the ones
// that overflow the screen have been lazy loaded.
TEST_F(BringAndroidTabsToIOSServiceTest, LazyLoadSomeInvisibleTabs) {
if (ui::GetDeviceFormFactor() != ui::DEVICE_FORM_FACTOR_PHONE) {
GTEST_SKIP() << "Feature unsupported on iPad";
}
int tab_per_session = kMaxNumberOfInstantLoadedTabs / kPhoneSessionCount + 1;
SetUpOpenTabsUIDelegate(tab_per_session, /*tabs_recently_active=*/true);
SetUpBringAndroidTabsServiceAndLoadTabs(/*is_android_switcher=*/true);
ExpectHistogram(bring_android_tabs::PromptAttemptStatus::kSuccess);
// Open first 7 of the 8 tabs. The last tab should be lazy loaded.
std::vector<size_t> indices(kMaxNumberOfInstantLoadedTabs + 1);
std::iota(std::begin(indices), std::end(indices), 0);
FakeUrlLoadingBrowserAgent* url_loader = GetTestUrlLoader();
bring_android_tabs_to_ios_service()->OpenTabsAtIndices(indices, url_loader);
EXPECT_EQ(kMaxNumberOfInstantLoadedTabs + 1,
url_loader->load_new_tab_call_count);
int last_tab_index = kMaxNumberOfInstantLoadedTabs;
EXPECT_EQ(GetTestURLSpec(last_tab_index),
url_loader->last_params.web_params.url.spec());
EXPECT_EQ(GetTestTitle(last_tab_index),
url_loader->last_params.placeholder_title);
EXPECT_FALSE(url_loader->last_params.instant_load);
}
// Tests that when there are many foreign tabs, only 20 tabs would be brought
// over.
TEST_F(BringAndroidTabsToIOSServiceTest, AvoidOpenTooManyTabs) {
if (ui::GetDeviceFormFactor() != ui::DEVICE_FORM_FACTOR_PHONE) {
GTEST_SKIP() << "Feature unsupported on iPad";
}
// We allow 20 tabs in total, so we set the number of sessions that exceeds
// this number.
int tab_per_session = kMaxNumberOfTabs / kPhoneSessionCount + 1;
SetUpOpenTabsUIDelegate(tab_per_session,
/*tabs_recently_active=*/true);
SetUpBringAndroidTabsServiceAndLoadTabs(/*is_android_switcher=*/true);
EXPECT_EQ(bring_android_tabs_to_ios_service()->GetNumberOfAndroidTabs(),
static_cast<size_t>(kMaxNumberOfTabs));
ExpectHistogram(bring_android_tabs::PromptAttemptStatus::kSuccess);
// Verify that only 20 tabs are loaded and can be opened.
FakeUrlLoadingBrowserAgent* url_loader = GetTestUrlLoader();
bring_android_tabs_to_ios_service()->OpenAllTabs(url_loader);
EXPECT_EQ(kMaxNumberOfTabs, url_loader->load_new_tab_call_count);
int last_tab_index = kMaxNumberOfTabs - 1;
EXPECT_EQ(GetTestURLSpec(last_tab_index),
url_loader->last_params.web_params.url.spec());
EXPECT_EQ(GetTestTitle(last_tab_index),
url_loader->last_params.placeholder_title);
EXPECT_FALSE(url_loader->last_params.instant_load);
}
// Tests that when there are foreign tabs with repeating title and URLs, they
// are not opened.
TEST_F(BringAndroidTabsToIOSServiceTest, AvoidOpenDuplicateTabs) {
if (ui::GetDeviceFormFactor() != ui::DEVICE_FORM_FACTOR_PHONE) {
GTEST_SKIP() << "Feature unsupported on iPad";
}
int tab_per_session = 2;
SetUpOpenTabsUIDelegateWithDuplicates(tab_per_session,
/*tabs_recently_active=*/true);
SetUpBringAndroidTabsServiceAndLoadTabs(/*is_android_switcher=*/true);
EXPECT_EQ(bring_android_tabs_to_ios_service()->GetNumberOfAndroidTabs(),
static_cast<size_t>(tab_per_session * kPhoneSessionCount));
ExpectHistogram(bring_android_tabs::PromptAttemptStatus::kSuccess);
FakeUrlLoadingBrowserAgent* url_loader = GetTestUrlLoader();
bring_android_tabs_to_ios_service()->OpenAllTabs(url_loader);
EXPECT_EQ(static_cast<int>(tab_per_session * kPhoneSessionCount),
url_loader->load_new_tab_call_count);
// Verify that only deduplicated tabs are loaded, by looking at the last
// loaded tab.
int last_tab_index = (tab_per_session * kPhoneSessionCount) - 1;
EXPECT_EQ(GetTestURLSpec(last_tab_index),
url_loader->last_params.web_params.url.spec());
EXPECT_EQ(GetTestTitle(last_tab_index),
url_loader->last_params.placeholder_title);
}