1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "chrome/browser/ui/ash/launcher/browser_launcher_item_controller.h" 6 7 #include <map> 8 #include <string> 9 10 #include "ash/ash_switches.h" 11 #include "ash/launcher/launcher_model.h" 12 #include "base/command_line.h" 13 #include "base/memory/scoped_ptr.h" 14 #include "chrome/browser/favicon/favicon_tab_helper.h" 15 #include "chrome/browser/ui/ash/launcher/chrome_launcher_controller.h" 16 #include "chrome/browser/ui/ash/launcher/launcher_item_controller.h" 17 #include "chrome/browser/ui/tabs/tab_strip_model.h" 18 #include "chrome/browser/ui/tabs/test_tab_strip_model_delegate.h" 19 #include "chrome/test/base/chrome_render_view_host_test_harness.h" 20 #include "chrome/test/base/testing_profile.h" 21 #include "content/public/browser/web_contents.h" 22 #include "testing/gtest/include/gtest/gtest.h" 23 #include "third_party/skia/include/core/SkBitmap.h" 24 #include "ui/aura/client/activation_change_observer.h" 25 #include "ui/aura/client/activation_delegate.h" 26 #include "ui/aura/client/aura_constants.h" 27 #include "ui/aura/root_window.h" 28 #include "ui/aura/test/test_activation_client.h" 29 #include "ui/aura/test/test_window_delegate.h" 30 #include "ui/aura/window.h" 31 #include "ui/aura/window_delegate.h" 32 #include "ui/base/events/event.h" 33 34 // TODO(skuhne): Remove this module together with the 35 // browser_launcher_item_controller.* when the old launcher goes away. 36 37 namespace { 38 39 // Test implementation of AppTabHelper. 40 class AppTabHelperImpl : public ChromeLauncherController::AppTabHelper { 41 public: 42 AppTabHelperImpl() {} 43 virtual ~AppTabHelperImpl() {} 44 45 // Sets the id for the specified tab. The id is removed if Remove() is 46 // invoked. 47 void SetAppID(content::WebContents* tab, const std::string& id) { 48 tab_id_map_[tab] = id; 49 } 50 51 // Returns true if there is an id registered for |tab|. 52 bool HasAppID(content::WebContents* tab) const { 53 return tab_id_map_.find(tab) != tab_id_map_.end(); 54 } 55 56 // AppTabHelper implementation: 57 virtual std::string GetAppID(content::WebContents* tab) OVERRIDE { 58 return tab_id_map_.find(tab) != tab_id_map_.end() ? tab_id_map_[tab] : 59 std::string(); 60 } 61 62 virtual bool IsValidID(const std::string& id) OVERRIDE { 63 for (TabToStringMap::const_iterator i = tab_id_map_.begin(); 64 i != tab_id_map_.end(); ++i) { 65 if (i->second == id) 66 return true; 67 } 68 return false; 69 } 70 71 private: 72 typedef std::map<content::WebContents*, std::string> TabToStringMap; 73 74 TabToStringMap tab_id_map_; 75 76 DISALLOW_COPY_AND_ASSIGN(AppTabHelperImpl); 77 }; 78 79 // Test implementation of AppIconLoader. 80 class AppIconLoaderImpl : public extensions::AppIconLoader { 81 public: 82 AppIconLoaderImpl() : fetch_count_(0) {} 83 virtual ~AppIconLoaderImpl() {} 84 85 // Returns the number of times FetchImage() has been invoked and resets the 86 // count to 0. 87 int GetAndClearFetchCount() { 88 int value = fetch_count_; 89 fetch_count_ = 0; 90 return value; 91 } 92 93 // AppIconLoader implementation: 94 virtual void FetchImage(const std::string& id) OVERRIDE { 95 fetch_count_++; 96 } 97 virtual void ClearImage(const std::string& id) OVERRIDE { 98 } 99 virtual void UpdateImage(const std::string& id) OVERRIDE { 100 } 101 102 private: 103 int fetch_count_; 104 105 DISALLOW_COPY_AND_ASSIGN(AppIconLoaderImpl); 106 }; 107 108 // Test implementation of TabStripModelDelegate. 109 class TabHelperTabStripModelDelegate : public TestTabStripModelDelegate { 110 public: 111 TabHelperTabStripModelDelegate() {} 112 virtual ~TabHelperTabStripModelDelegate() {} 113 114 virtual void WillAddWebContents(content::WebContents* contents) OVERRIDE { 115 // BrowserLauncherItemController assumes that all WebContents passed to it 116 // have attached an extensions::TabHelper and a FaviconTabHelper. The 117 // TestTabStripModelDelegate adds an extensions::TabHelper. 118 TestTabStripModelDelegate::WillAddWebContents(contents); 119 FaviconTabHelper::CreateForWebContents(contents); 120 } 121 122 private: 123 DISALLOW_COPY_AND_ASSIGN(TabHelperTabStripModelDelegate); 124 }; 125 126 } // namespace 127 128 // TODO(skuhne): Several of these unit tests need to be moved into a new home 129 // when the old launcher & the browser launcher item controller are removed 130 // (several of these tests are not testing the BrowserLauncherItemController - 131 // but the LauncherController framework). 132 class LauncherItemControllerPerAppTest 133 : public ChromeRenderViewHostTestHarness { 134 public: 135 virtual void SetUp() OVERRIDE { 136 ChromeRenderViewHostTestHarness::SetUp(); 137 138 activation_client_.reset( 139 new aura::test::TestActivationClient(root_window())); 140 launcher_model_.reset(new ash::LauncherModel); 141 launcher_delegate_.reset( 142 ChromeLauncherController::CreateInstance(profile(), 143 launcher_model_.get())); 144 app_tab_helper_ = new AppTabHelperImpl; 145 app_icon_loader_ = new AppIconLoaderImpl; 146 launcher_delegate_->SetAppTabHelperForTest(app_tab_helper_); 147 launcher_delegate_->SetAppIconLoaderForTest(app_icon_loader_); 148 launcher_delegate_->Init(); 149 } 150 151 virtual void TearDown() OVERRIDE { 152 launcher_delegate_.reset(); 153 ChromeRenderViewHostTestHarness::TearDown(); 154 } 155 156 protected: 157 // Contains all the objects needed to create a BrowserLauncherItemController. 158 struct State : public aura::client::ActivationDelegate, 159 public aura::client::ActivationChangeObserver { 160 public: 161 State(LauncherItemControllerPerAppTest* test, 162 const std::string& app_id, 163 BrowserLauncherItemController::Type launcher_type) 164 : launcher_test(test), 165 window(NULL), 166 tab_strip(&tab_strip_delegate, test->profile()), 167 updater(launcher_type, 168 &window, 169 &tab_strip, 170 test->launcher_delegate_.get(), 171 app_id) { 172 window.Init(ui::LAYER_NOT_DRAWN); 173 launcher_test->root_window()->AddChild(&window); 174 launcher_test->activation_client_->ActivateWindow(&window); 175 aura::client::SetActivationDelegate(&window, this); 176 aura::client::SetActivationChangeObserver(&window, this); 177 updater.Init(); 178 } 179 180 ash::LauncherItem GetUpdaterItem() { 181 ash::LauncherID launcher_id = 182 BrowserLauncherItemController::TestApi(&updater).item_id(); 183 int index = launcher_test->launcher_model_->ItemIndexByID(launcher_id); 184 return launcher_test->launcher_model_->items()[index]; 185 } 186 187 // aura::client::ActivationDelegate overrides. 188 virtual bool ShouldActivate() const OVERRIDE { 189 return true; 190 } 191 192 // aura::client::ActivationChangeObserver overrides: 193 virtual void OnWindowActivated(aura::Window* gained_active, 194 aura::Window* lost_active) OVERRIDE { 195 DCHECK(&window == gained_active || &window == lost_active); 196 updater.BrowserActivationStateChanged(); 197 } 198 199 LauncherItemControllerPerAppTest* launcher_test; 200 aura::Window window; 201 TabHelperTabStripModelDelegate tab_strip_delegate; 202 TabStripModel tab_strip; 203 BrowserLauncherItemController updater; 204 205 private: 206 DISALLOW_COPY_AND_ASSIGN(State); 207 }; 208 209 const std::string& GetAppID(ash::LauncherID id) const { 210 return launcher_delegate_->GetAppIdFromLauncherIdForTest(id); 211 } 212 213 void ResetAppTabHelper() { 214 launcher_delegate_->SetAppTabHelperForTest(app_tab_helper_); 215 } 216 217 void ResetAppIconLoader() { 218 launcher_delegate_->SetAppIconLoaderForTest(app_icon_loader_); 219 } 220 221 void UnpinAppsWithID(const std::string& app_id) { 222 launcher_delegate_->UnpinAppsWithID(app_id); 223 } 224 225 const ash::LauncherItem& GetItem(BrowserLauncherItemController* updater) { 226 int index = launcher_model_->ItemIndexByID( 227 BrowserLauncherItemController::TestApi(updater).item_id()); 228 return launcher_model_->items()[index]; 229 } 230 231 scoped_ptr<ash::LauncherModel> launcher_model_; 232 scoped_ptr<ChromeLauncherController> launcher_delegate_; 233 234 // Owned by BrowserLauncherItemController. 235 AppTabHelperImpl* app_tab_helper_; 236 AppIconLoaderImpl* app_icon_loader_; 237 238 scoped_ptr<aura::test::TestActivationClient> activation_client_; 239 }; 240 241 // Verify that the launcher item positions are persisted and restored. 242 TEST_F(LauncherItemControllerPerAppTest, PersistLauncherItemPositions) { 243 EXPECT_EQ(ash::TYPE_BROWSER_SHORTCUT, 244 launcher_model_->items()[0].type); 245 EXPECT_EQ(ash::TYPE_APP_LIST, 246 launcher_model_->items()[1].type); 247 scoped_ptr<content::WebContents> tab1(CreateTestWebContents()); 248 scoped_ptr<content::WebContents> tab2(CreateTestWebContents()); 249 app_tab_helper_->SetAppID(tab1.get(), "1"); 250 app_tab_helper_->SetAppID(tab1.get(), "2"); 251 252 EXPECT_FALSE(launcher_delegate_->IsAppPinned("1")); 253 launcher_delegate_->PinAppWithID("1"); 254 EXPECT_TRUE(launcher_delegate_->IsAppPinned("1")); 255 launcher_delegate_->PinAppWithID("2"); 256 257 EXPECT_EQ(ash::TYPE_BROWSER_SHORTCUT, 258 launcher_model_->items()[0].type); 259 EXPECT_EQ(ash::TYPE_APP_SHORTCUT, 260 launcher_model_->items()[1].type); 261 EXPECT_EQ(ash::TYPE_APP_SHORTCUT, 262 launcher_model_->items()[2].type); 263 EXPECT_EQ(ash::TYPE_APP_LIST, 264 launcher_model_->items()[3].type); 265 266 launcher_model_->Move(0, 2); 267 EXPECT_EQ(ash::TYPE_APP_SHORTCUT, 268 launcher_model_->items()[0].type); 269 EXPECT_EQ(ash::TYPE_APP_SHORTCUT, 270 launcher_model_->items()[1].type); 271 EXPECT_EQ(ash::TYPE_BROWSER_SHORTCUT, 272 launcher_model_->items()[2].type); 273 EXPECT_EQ(ash::TYPE_APP_LIST, 274 launcher_model_->items()[3].type); 275 276 launcher_delegate_.reset(); 277 launcher_model_.reset(new ash::LauncherModel); 278 launcher_delegate_.reset( 279 ChromeLauncherController::CreateInstance(profile(), 280 launcher_model_.get())); 281 app_tab_helper_ = new AppTabHelperImpl; 282 app_tab_helper_->SetAppID(tab1.get(), "1"); 283 app_tab_helper_->SetAppID(tab2.get(), "2"); 284 ResetAppTabHelper(); 285 286 launcher_delegate_->Init(); 287 288 EXPECT_EQ(ash::TYPE_APP_SHORTCUT, 289 launcher_model_->items()[0].type); 290 EXPECT_EQ(ash::TYPE_APP_SHORTCUT, 291 launcher_model_->items()[1].type); 292 EXPECT_EQ(ash::TYPE_BROWSER_SHORTCUT, 293 launcher_model_->items()[2].type); 294 EXPECT_EQ(ash::TYPE_APP_LIST, 295 launcher_model_->items()[3].type); 296 } 297 298 class BrowserLauncherItemControllerTest 299 : public LauncherItemControllerPerAppTest { 300 public: 301 BrowserLauncherItemControllerTest() {} 302 303 virtual void SetUp() OVERRIDE { 304 CommandLine::ForCurrentProcess()->AppendSwitch( 305 ash::switches::kAshDisablePerAppLauncher); 306 307 LauncherItemControllerPerAppTest::SetUp(); 308 } 309 310 virtual void TearDown() OVERRIDE { 311 LauncherItemControllerPerAppTest::TearDown(); 312 } 313 }; 314 315 // Verifies a new launcher item is added for TYPE_TABBED. 316 TEST_F(BrowserLauncherItemControllerTest, TabbedSetup) { 317 size_t initial_size = launcher_model_->items().size(); 318 { 319 scoped_ptr<content::WebContents> web_contents(CreateTestWebContents()); 320 State state(this, std::string(), 321 BrowserLauncherItemController::TYPE_TABBED); 322 323 // There should be one more item. 324 ASSERT_EQ(initial_size + 1, launcher_model_->items().size()); 325 // New item should be added at the end. 326 EXPECT_EQ(ash::TYPE_TABBED, state.GetUpdaterItem().type); 327 } 328 329 // Deleting the BrowserLauncherItemController should have removed the item. 330 ASSERT_EQ(initial_size, launcher_model_->items().size()); 331 332 // Do the same, but this time add the tab first. 333 { 334 scoped_ptr<content::WebContents> web_contents(CreateTestWebContents()); 335 336 TabHelperTabStripModelDelegate tab_strip_delegate; 337 TabStripModel tab_strip(&tab_strip_delegate, profile()); 338 tab_strip.InsertWebContentsAt(0, 339 web_contents.get(), 340 TabStripModel::ADD_ACTIVE); 341 aura::Window window(NULL); 342 window.Init(ui::LAYER_NOT_DRAWN); 343 root_window()->AddChild(&window); 344 BrowserLauncherItemController updater( 345 LauncherItemController::TYPE_TABBED, 346 &window, &tab_strip, launcher_delegate_.get(), 347 std::string()); 348 updater.Init(); 349 350 // There should be one more item. 351 ASSERT_EQ(initial_size + 1, launcher_model_->items().size()); 352 // New item should be added at the end. 353 EXPECT_EQ(ash::TYPE_TABBED, GetItem(&updater).type); 354 } 355 } 356 357 // Verifies pinned apps are persisted and restored. 358 TEST_F(BrowserLauncherItemControllerTest, PersistPinned) { 359 size_t initial_size = launcher_model_->items().size(); 360 scoped_ptr<content::WebContents> tab1(CreateTestWebContents()); 361 362 app_tab_helper_->SetAppID(tab1.get(), "1"); 363 364 app_icon_loader_->GetAndClearFetchCount(); 365 launcher_delegate_->PinAppWithID("1"); 366 ash::LauncherID id = launcher_delegate_->GetLauncherIDForAppID("1"); 367 int app_index = launcher_model_->ItemIndexByID(id); 368 EXPECT_GT(app_icon_loader_->GetAndClearFetchCount(), 0); 369 EXPECT_EQ(ash::TYPE_APP_SHORTCUT, 370 launcher_model_->items()[app_index].type); 371 EXPECT_TRUE(launcher_delegate_->IsAppPinned("1")); 372 EXPECT_FALSE(launcher_delegate_->IsAppPinned("0")); 373 EXPECT_EQ(initial_size + 1, launcher_model_->items().size()); 374 375 launcher_delegate_.reset(); 376 launcher_model_.reset(new ash::LauncherModel); 377 launcher_delegate_.reset( 378 ChromeLauncherController::CreateInstance(profile(), 379 launcher_model_.get())); 380 app_tab_helper_ = new AppTabHelperImpl; 381 app_tab_helper_->SetAppID(tab1.get(), "1"); 382 ResetAppTabHelper(); 383 app_icon_loader_ = new AppIconLoaderImpl; 384 ResetAppIconLoader(); 385 launcher_delegate_->Init(); 386 EXPECT_GT(app_icon_loader_->GetAndClearFetchCount(), 0); 387 ASSERT_EQ(initial_size + 1, launcher_model_->items().size()); 388 EXPECT_TRUE(launcher_delegate_->IsAppPinned("1")); 389 EXPECT_FALSE(launcher_delegate_->IsAppPinned("0")); 390 EXPECT_EQ(ash::TYPE_APP_SHORTCUT, 391 launcher_model_->items()[app_index].type); 392 393 UnpinAppsWithID("1"); 394 ASSERT_EQ(initial_size, launcher_model_->items().size()); 395 } 396 397 // Verify that launcher item positions are persisted and restored. 398 TEST_F(BrowserLauncherItemControllerTest, 399 PersistLauncherItemPositionsPerBrowser) { 400 int browser_shortcut_index = 0; 401 int app_list_index = 1; 402 403 EXPECT_EQ(ash::TYPE_BROWSER_SHORTCUT, 404 launcher_model_->items()[browser_shortcut_index].type); 405 EXPECT_EQ(ash::TYPE_APP_LIST, 406 launcher_model_->items()[app_list_index].type); 407 408 scoped_ptr<content::WebContents> tab1(CreateTestWebContents()); 409 scoped_ptr<content::WebContents> tab2(CreateTestWebContents()); 410 411 app_tab_helper_->SetAppID(tab1.get(), "1"); 412 app_tab_helper_->SetAppID(tab2.get(), "2"); 413 414 app_icon_loader_->GetAndClearFetchCount(); 415 launcher_delegate_->PinAppWithID("1"); 416 ash::LauncherID id = launcher_delegate_->GetLauncherIDForAppID("1"); 417 int app1_index = launcher_model_->ItemIndexByID(id); 418 419 launcher_delegate_->PinAppWithID("2"); 420 id = launcher_delegate_->GetLauncherIDForAppID("2"); 421 int app2_index = launcher_model_->ItemIndexByID(id); 422 423 launcher_model_->Move(browser_shortcut_index, app1_index); 424 425 browser_shortcut_index = 1; 426 app1_index = 0; 427 428 EXPECT_GT(app_icon_loader_->GetAndClearFetchCount(), 0); 429 EXPECT_EQ(ash::TYPE_APP_SHORTCUT, 430 launcher_model_->items()[app1_index].type); 431 EXPECT_EQ(ash::TYPE_BROWSER_SHORTCUT, 432 launcher_model_->items()[browser_shortcut_index].type); 433 EXPECT_EQ(ash::TYPE_APP_SHORTCUT, 434 launcher_model_->items()[app2_index].type); 435 436 launcher_delegate_.reset(); 437 launcher_model_.reset(new ash::LauncherModel); 438 launcher_delegate_.reset( 439 ChromeLauncherController::CreateInstance(profile(), 440 launcher_model_.get())); 441 442 app_tab_helper_ = new AppTabHelperImpl; 443 app_tab_helper_->SetAppID(tab1.get(), "1"); 444 app_tab_helper_->SetAppID(tab2.get(), "2"); 445 ResetAppTabHelper(); 446 app_icon_loader_ = new AppIconLoaderImpl; 447 ResetAppIconLoader(); 448 launcher_delegate_->Init(); 449 450 EXPECT_EQ(ash::TYPE_APP_SHORTCUT, 451 launcher_model_->items()[app1_index].type); 452 EXPECT_EQ(ash::TYPE_BROWSER_SHORTCUT, 453 launcher_model_->items()[browser_shortcut_index].type); 454 EXPECT_EQ(ash::TYPE_APP_SHORTCUT, 455 launcher_model_->items()[app2_index].type); 456 } 457 458 // Confirm that tabbed browsers handle activation correctly. 459 TEST_F(BrowserLauncherItemControllerTest, ActivateBrowsers) { 460 State state1(this, std::string(), BrowserLauncherItemController::TYPE_TABBED); 461 462 // First browser is active. 463 EXPECT_EQ(ash::STATUS_ACTIVE, state1.GetUpdaterItem().status); 464 465 { 466 // Both running. 467 State state2(this, std::string(), 468 BrowserLauncherItemController::TYPE_TABBED); 469 EXPECT_EQ(ash::STATUS_ACTIVE, state2.GetUpdaterItem().status); 470 EXPECT_EQ(ash::STATUS_RUNNING, state1.GetUpdaterItem().status); 471 472 // Make first browser active again. 473 activation_client_->ActivateWindow(&state1.window); 474 EXPECT_EQ(ash::STATUS_ACTIVE, state1.GetUpdaterItem().status); 475 EXPECT_EQ(ash::STATUS_RUNNING, state2.GetUpdaterItem().status); 476 477 // And back to second. 478 activation_client_->ActivateWindow(&state2.window); 479 EXPECT_EQ(ash::STATUS_ACTIVE, state2.GetUpdaterItem().status); 480 EXPECT_EQ(ash::STATUS_RUNNING, state1.GetUpdaterItem().status); 481 } 482 483 // First browser should be active again after second is closed. 484 EXPECT_EQ(ash::STATUS_ACTIVE, state1.GetUpdaterItem().status); 485 } 486 487 // Confirm that window activation works through the model. 488 TEST_F(BrowserLauncherItemControllerTest, SwitchDirectlyToApp) { 489 State state1(this, std::string(), 490 BrowserLauncherItemController::TYPE_TABBED); 491 int index1 = launcher_model_->ItemIndexByID(state1.GetUpdaterItem().id); 492 493 // Second app is active and first is inactive. 494 State state2(this, std::string(), 495 BrowserLauncherItemController::TYPE_TABBED); 496 int index2 = launcher_model_->ItemIndexByID(state2.GetUpdaterItem().id); 497 498 EXPECT_EQ(ash::STATUS_RUNNING, state1.GetUpdaterItem().status); 499 EXPECT_EQ(ash::STATUS_ACTIVE, state2.GetUpdaterItem().status); 500 EXPECT_EQ(&state2.window, activation_client_->GetActiveWindow()); 501 502 // Test that we can properly switch to the first item. 503 ash::LauncherItem new_item1(launcher_model_->items()[index1]); 504 new_item1.status = ash::STATUS_ACTIVE; 505 launcher_model_->Set(index1, new_item1); 506 EXPECT_EQ(ash::STATUS_ACTIVE, launcher_model_->items()[index1].status); 507 EXPECT_EQ(ash::STATUS_RUNNING, launcher_model_->items()[index2].status); 508 EXPECT_EQ(&state1.window, activation_client_->GetActiveWindow()); 509 510 // And to the second item active. 511 ash::LauncherItem new_item2(launcher_model_->items()[index2]); 512 new_item2.status = ash::STATUS_ACTIVE; 513 launcher_model_->Set(index2, new_item2); 514 EXPECT_EQ(ash::STATUS_RUNNING, launcher_model_->items()[index1].status); 515 EXPECT_EQ(ash::STATUS_ACTIVE, launcher_model_->items()[index2].status); 516 EXPECT_EQ(&state2.window, activation_client_->GetActiveWindow()); 517 } 518 519 // Test attention states of windows. 520 TEST_F(BrowserLauncherItemControllerTest, FlashWindow) { 521 // App panel first 522 State app_state(this, "1", BrowserLauncherItemController::TYPE_APP_PANEL); 523 EXPECT_EQ(ash::STATUS_ACTIVE, app_state.GetUpdaterItem().status); 524 525 // Active windows don't show attention. 526 app_state.window.SetProperty(aura::client::kDrawAttentionKey, true); 527 EXPECT_EQ(ash::STATUS_ACTIVE, app_state.GetUpdaterItem().status); 528 529 // Then browser window 530 State browser_state( 531 this, std::string(), BrowserLauncherItemController::TYPE_TABBED); 532 // First browser is active. 533 EXPECT_EQ(ash::STATUS_ACTIVE, browser_state.GetUpdaterItem().status); 534 EXPECT_EQ(ash::STATUS_RUNNING, app_state.GetUpdaterItem().status); 535 536 // App window should go to attention state. 537 app_state.window.SetProperty(aura::client::kDrawAttentionKey, true); 538 EXPECT_EQ(ash::STATUS_ATTENTION, app_state.GetUpdaterItem().status); 539 540 // Activating app window should clear attention state. 541 activation_client_->ActivateWindow(&app_state.window); 542 EXPECT_EQ(ash::STATUS_ACTIVE, app_state.GetUpdaterItem().status); 543 } 544