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 "build/build_config.h" 6 #include "chrome/browser/ui/browser.h" 7 #include "chrome/browser/ui/browser_tabstrip.h" 8 #include "chrome/browser/ui/fullscreen/fullscreen_controller.h" 9 #include "chrome/browser/ui/fullscreen/fullscreen_controller_state_test.h" 10 #include "chrome/browser/ui/tabs/tab_strip_model.h" 11 #include "chrome/test/base/browser_with_test_window_test.h" 12 #include "content/public/browser/web_contents.h" 13 #include "content/public/common/url_constants.h" 14 #include "testing/gtest/include/gtest/gtest.h" 15 16 // The FullscreenControllerStateUnitTest unit test suite exhastively tests 17 // the FullscreenController through all permutations of events. The behavior 18 // of the BrowserWindow is mocked via FullscreenControllerTestWindow. 19 20 21 // FullscreenControllerTestWindow ---------------------------------------------- 22 23 // A BrowserWindow used for testing FullscreenController. The behavior of this 24 // mock is verfied manually by running FullscreenControllerStateInteractiveTest. 25 class FullscreenControllerTestWindow : public TestBrowserWindow { 26 public: 27 // Simulate the window state with an enumeration. 28 enum WindowState { 29 NORMAL, 30 FULLSCREEN, 31 // No TO_ state for METRO_SNAP, the windows implementation is synchronous. 32 METRO_SNAP, 33 TO_NORMAL, 34 TO_FULLSCREEN, 35 }; 36 37 FullscreenControllerTestWindow(); 38 virtual ~FullscreenControllerTestWindow() {} 39 40 // BrowserWindow Interface: 41 virtual void EnterFullscreen(const GURL& url, 42 FullscreenExitBubbleType type) OVERRIDE; 43 virtual void ExitFullscreen() OVERRIDE; 44 virtual bool ShouldHideUIForFullscreen() const OVERRIDE; 45 virtual bool IsFullscreen() const OVERRIDE; 46 #if defined(OS_WIN) 47 virtual void SetMetroSnapMode(bool enable) OVERRIDE; 48 virtual bool IsInMetroSnapMode() const OVERRIDE; 49 #endif 50 #if defined(OS_MACOSX) 51 virtual void EnterFullscreenWithChrome() OVERRIDE; 52 virtual bool IsFullscreenWithChrome() OVERRIDE; 53 virtual bool IsFullscreenWithoutChrome() OVERRIDE; 54 #endif 55 56 static const char* GetWindowStateString(WindowState state); 57 WindowState state() const { return state_; } 58 void set_browser(Browser* browser) { browser_ = browser; } 59 60 // Simulates the window changing state. 61 void ChangeWindowFullscreenState(); 62 63 private: 64 // Enters fullscreen with |new_mac_with_chrome_mode|. 65 void EnterFullscreen(bool new_mac_with_chrome_mode); 66 67 // Returns true if ChangeWindowFullscreenState() should be called as a result 68 // of updating the current fullscreen state to the passed in state. 69 bool IsTransitionReentrant(bool new_fullscreen, 70 bool new_mac_with_chrome_mode); 71 72 WindowState state_; 73 bool mac_with_chrome_mode_; 74 Browser* browser_; 75 }; 76 77 FullscreenControllerTestWindow::FullscreenControllerTestWindow() 78 : state_(NORMAL), 79 mac_with_chrome_mode_(false), 80 browser_(NULL) { 81 } 82 83 void FullscreenControllerTestWindow::EnterFullscreen( 84 const GURL& url, FullscreenExitBubbleType type) { 85 EnterFullscreen(false); 86 } 87 88 void FullscreenControllerTestWindow::ExitFullscreen() { 89 if (IsFullscreen()) { 90 state_ = TO_NORMAL; 91 mac_with_chrome_mode_ = false; 92 93 if (IsTransitionReentrant(false, false)) 94 ChangeWindowFullscreenState(); 95 } 96 } 97 98 bool FullscreenControllerTestWindow::ShouldHideUIForFullscreen() const { 99 return IsFullscreen(); 100 } 101 102 bool FullscreenControllerTestWindow::IsFullscreen() const { 103 #if defined(OS_MACOSX) 104 return state_ == FULLSCREEN || state_ == TO_FULLSCREEN; 105 #else 106 return state_ == FULLSCREEN || state_ == TO_NORMAL; 107 #endif 108 } 109 110 #if defined(OS_WIN) 111 void FullscreenControllerTestWindow::SetMetroSnapMode(bool enable) { 112 if (enable != IsInMetroSnapMode()) 113 state_ = enable ? METRO_SNAP : NORMAL; 114 115 if (FullscreenControllerStateTest::IsWindowFullscreenStateChangedReentrant()) 116 ChangeWindowFullscreenState(); 117 } 118 119 bool FullscreenControllerTestWindow::IsInMetroSnapMode() const { 120 return state_ == METRO_SNAP; 121 } 122 #endif 123 124 #if defined(OS_MACOSX) 125 void FullscreenControllerTestWindow::EnterFullscreenWithChrome() { 126 EnterFullscreen(true); 127 } 128 129 bool FullscreenControllerTestWindow::IsFullscreenWithChrome() { 130 return IsFullscreen() && mac_with_chrome_mode_; 131 } 132 133 bool FullscreenControllerTestWindow::IsFullscreenWithoutChrome() { 134 return IsFullscreen() && !mac_with_chrome_mode_; 135 } 136 #endif 137 138 // static 139 const char* FullscreenControllerTestWindow::GetWindowStateString( 140 WindowState state) { 141 switch (state) { 142 ENUM_TO_STRING(NORMAL); 143 ENUM_TO_STRING(FULLSCREEN); 144 ENUM_TO_STRING(METRO_SNAP); 145 ENUM_TO_STRING(TO_FULLSCREEN); 146 ENUM_TO_STRING(TO_NORMAL); 147 default: 148 NOTREACHED() << "No string for state " << state; 149 return "WindowState-Unknown"; 150 } 151 } 152 153 void FullscreenControllerTestWindow::ChangeWindowFullscreenState() { 154 // Most states result in "no operation" intentionally. The tests 155 // assume that all possible states and event pairs can be tested, even 156 // though window managers will not generate all of these. 157 if (state_ == TO_FULLSCREEN) 158 state_ = FULLSCREEN; 159 else if (state_ == TO_NORMAL) 160 state_ = NORMAL; 161 162 // Emit a change event from every state to ensure the Fullscreen Controller 163 // handles it in all circumstances. 164 browser_->WindowFullscreenStateChanged(); 165 } 166 167 void FullscreenControllerTestWindow::EnterFullscreen( 168 bool new_mac_with_chrome_mode) { 169 bool reentrant = IsTransitionReentrant(true, new_mac_with_chrome_mode); 170 171 mac_with_chrome_mode_ = new_mac_with_chrome_mode; 172 if (!IsFullscreen()) 173 state_ = TO_FULLSCREEN; 174 175 if (reentrant) 176 ChangeWindowFullscreenState(); 177 } 178 179 bool FullscreenControllerTestWindow::IsTransitionReentrant( 180 bool new_fullscreen, 181 bool new_mac_with_chrome_mode) { 182 #if defined(OS_MACOSX) 183 bool mac_with_chrome_mode_changed = new_mac_with_chrome_mode ? 184 IsFullscreenWithoutChrome() : IsFullscreenWithChrome(); 185 #else 186 bool mac_with_chrome_mode_changed = false; 187 #endif 188 bool fullscreen_changed = (new_fullscreen != IsFullscreen()); 189 190 if (!fullscreen_changed && !mac_with_chrome_mode_changed) 191 return false; 192 193 if (FullscreenControllerStateTest::IsWindowFullscreenStateChangedReentrant()) 194 return true; 195 196 // BrowserWindowCocoa::EnterFullscreen() and 197 // BrowserWindowCocoa::EnterFullscreenWithChrome() are reentrant when 198 // switching between fullscreen with chrome and fullscreen without chrome. 199 return state_ == FULLSCREEN && 200 !fullscreen_changed && 201 mac_with_chrome_mode_changed; 202 } 203 204 205 // FullscreenControllerStateUnitTest ------------------------------------------- 206 207 // Unit test fixture testing Fullscreen Controller through its states. Most of 208 // the test logic comes from FullscreenControllerStateTest. 209 class FullscreenControllerStateUnitTest : public BrowserWithTestWindowTest, 210 public FullscreenControllerStateTest { 211 public: 212 FullscreenControllerStateUnitTest(); 213 214 // FullscreenControllerStateTest: 215 virtual void SetUp() OVERRIDE; 216 virtual BrowserWindow* CreateBrowserWindow() OVERRIDE; 217 virtual void ChangeWindowFullscreenState() OVERRIDE; 218 virtual const char* GetWindowStateString() OVERRIDE; 219 virtual void VerifyWindowState() OVERRIDE; 220 221 protected: 222 // FullscreenControllerStateTest: 223 virtual bool ShouldSkipStateAndEventPair(State state, Event event) OVERRIDE; 224 virtual Browser* GetBrowser() OVERRIDE; 225 FullscreenControllerTestWindow* window_; 226 }; 227 228 FullscreenControllerStateUnitTest::FullscreenControllerStateUnitTest () 229 : window_(NULL) { 230 } 231 232 void FullscreenControllerStateUnitTest::SetUp() { 233 BrowserWithTestWindowTest::SetUp(); 234 window_->set_browser(browser()); 235 } 236 237 BrowserWindow* FullscreenControllerStateUnitTest::CreateBrowserWindow() { 238 window_ = new FullscreenControllerTestWindow(); 239 return window_; // BrowserWithTestWindowTest takes ownership. 240 } 241 242 void FullscreenControllerStateUnitTest::ChangeWindowFullscreenState() { 243 window_->ChangeWindowFullscreenState(); 244 } 245 246 const char* FullscreenControllerStateUnitTest::GetWindowStateString() { 247 return FullscreenControllerTestWindow::GetWindowStateString(window_->state()); 248 } 249 250 void FullscreenControllerStateUnitTest::VerifyWindowState() { 251 switch (state()) { 252 case STATE_NORMAL: 253 EXPECT_EQ(FullscreenControllerTestWindow::NORMAL, 254 window_->state()) << GetAndClearDebugLog(); 255 break; 256 257 case STATE_BROWSER_FULLSCREEN_NO_CHROME: 258 case STATE_BROWSER_FULLSCREEN_WITH_CHROME: 259 case STATE_TAB_FULLSCREEN: 260 case STATE_TAB_BROWSER_FULLSCREEN: 261 case STATE_TAB_BROWSER_FULLSCREEN_CHROME: 262 EXPECT_EQ(FullscreenControllerTestWindow::FULLSCREEN, 263 window_->state()) << GetAndClearDebugLog(); 264 break; 265 266 #if defined(OS_WIN) 267 case STATE_METRO_SNAP: 268 EXPECT_EQ(FullscreenControllerTestWindow::METRO_SNAP, 269 window_->state()) << GetAndClearDebugLog(); 270 break; 271 #endif 272 273 case STATE_TO_NORMAL: 274 EXPECT_EQ(FullscreenControllerTestWindow::TO_NORMAL, 275 window_->state()) << GetAndClearDebugLog(); 276 break; 277 278 case STATE_TO_BROWSER_FULLSCREEN_NO_CHROME: 279 case STATE_TO_BROWSER_FULLSCREEN_WITH_CHROME: 280 case STATE_TO_TAB_FULLSCREEN: 281 EXPECT_EQ(FullscreenControllerTestWindow::TO_FULLSCREEN, 282 window_->state()) << GetAndClearDebugLog(); 283 break; 284 285 default: 286 NOTREACHED() << GetAndClearDebugLog(); 287 } 288 289 FullscreenControllerStateTest::VerifyWindowState(); 290 } 291 292 bool FullscreenControllerStateUnitTest::ShouldSkipStateAndEventPair( 293 State state, Event event) { 294 #if defined(OS_MACOSX) 295 // TODO(scheib) Toggle, Window Event, Toggle, Toggle on Mac as exposed by 296 // test *.STATE_TO_NORMAL__TOGGLE_FULLSCREEN runs interactively and exits to 297 // Normal. This doesn't appear to be the desired result, and would add 298 // too much complexity to mimic in our simple FullscreenControllerTestWindow. 299 // http://crbug.com/156968 300 if ((state == STATE_TO_NORMAL || 301 state == STATE_TO_BROWSER_FULLSCREEN_NO_CHROME || 302 state == STATE_TO_TAB_FULLSCREEN) && 303 event == TOGGLE_FULLSCREEN) 304 return true; 305 #endif 306 307 return FullscreenControllerStateTest::ShouldSkipStateAndEventPair(state, 308 event); 309 } 310 311 Browser* FullscreenControllerStateUnitTest::GetBrowser() { 312 return BrowserWithTestWindowTest::browser(); 313 } 314 315 316 // Soak tests ------------------------------------------------------------------ 317 318 // Tests all states with all permutations of multiple events to detect lingering 319 // state issues that would bleed over to other states. 320 // I.E. for each state test all combinations of events E1, E2, E3. 321 // 322 // This produces coverage for event sequences that may happen normally but 323 // would not be exposed by traversing to each state via TransitionToState(). 324 // TransitionToState() always takes the same path even when multiple paths 325 // exist. 326 TEST_F(FullscreenControllerStateUnitTest, TransitionsForEachState) { 327 // A tab is needed for tab fullscreen. 328 AddTab(browser(), GURL(content::kAboutBlankURL)); 329 TestTransitionsForEachState(); 330 // Progress of test can be examined via LOG(INFO) << GetAndClearDebugLog(); 331 } 332 333 334 // Individual tests for each pair of state and event --------------------------- 335 336 #define TEST_EVENT(state, event) \ 337 TEST_F(FullscreenControllerStateUnitTest, state##__##event) { \ 338 AddTab(browser(), GURL(content::kAboutBlankURL)); \ 339 ASSERT_NO_FATAL_FAILURE(TestStateAndEvent(state, event)) \ 340 << GetAndClearDebugLog(); \ 341 } 342 // Progress of tests can be examined by inserting the following line: 343 // LOG(INFO) << GetAndClearDebugLog(); } 344 345 #include "chrome/browser/ui/fullscreen/fullscreen_controller_state_tests.h" 346 347 348 // Specific one-off tests for known issues ------------------------------------- 349 350 // TODO(scheib) Toggling Tab fullscreen while pending Tab or 351 // Browser fullscreen is broken currently http://crbug.com/154196 352 TEST_F(FullscreenControllerStateUnitTest, 353 DISABLED_ToggleTabWhenPendingBrowser) { 354 // Only possible without reentrancy. 355 if (FullscreenControllerStateTest::IsWindowFullscreenStateChangedReentrant()) 356 return; 357 AddTab(browser(), GURL(content::kAboutBlankURL)); 358 ASSERT_NO_FATAL_FAILURE( 359 TransitionToState(STATE_TO_BROWSER_FULLSCREEN_NO_CHROME)) 360 << GetAndClearDebugLog(); 361 362 ASSERT_TRUE(InvokeEvent(TAB_FULLSCREEN_TRUE)) << GetAndClearDebugLog(); 363 ASSERT_TRUE(InvokeEvent(TAB_FULLSCREEN_FALSE)) << GetAndClearDebugLog(); 364 ASSERT_TRUE(InvokeEvent(WINDOW_CHANGE)) << GetAndClearDebugLog(); 365 } 366 367 // TODO(scheib) Toggling Tab fullscreen while pending Tab or 368 // Browser fullscreen is broken currently http://crbug.com/154196 369 TEST_F(FullscreenControllerStateUnitTest, DISABLED_ToggleTabWhenPendingTab) { 370 // Only possible without reentrancy. 371 if (FullscreenControllerStateTest::IsWindowFullscreenStateChangedReentrant()) 372 return; 373 AddTab(browser(), GURL(content::kAboutBlankURL)); 374 ASSERT_NO_FATAL_FAILURE( 375 TransitionToState(STATE_TO_TAB_FULLSCREEN)) 376 << GetAndClearDebugLog(); 377 378 ASSERT_TRUE(InvokeEvent(TAB_FULLSCREEN_TRUE)) << GetAndClearDebugLog(); 379 ASSERT_TRUE(InvokeEvent(TAB_FULLSCREEN_FALSE)) << GetAndClearDebugLog(); 380 ASSERT_TRUE(InvokeEvent(WINDOW_CHANGE)) << GetAndClearDebugLog(); 381 } 382 383 // Debugging utility: Display the transition tables. Intentionally disabled 384 TEST_F(FullscreenControllerStateUnitTest, DISABLED_DebugLogStateTables) { 385 std::ostringstream output; 386 output << "\n\nTransition Table:"; 387 output << GetTransitionTableAsString(); 388 389 output << "\n\nInitial transitions:"; 390 output << GetStateTransitionsAsString(); 391 392 // Calculate all transition pairs. 393 for (int state1_int = 0; state1_int < NUM_STATES; ++state1_int) { 394 State state1 = static_cast<State>(state1_int); 395 for (int state2_int = 0; state2_int < NUM_STATES; ++state2_int) { 396 State state2 = static_cast<State>(state2_int); 397 if (ShouldSkipStateAndEventPair(state1, EVENT_INVALID) || 398 ShouldSkipStateAndEventPair(state2, EVENT_INVALID)) 399 continue; 400 // Compute the transition 401 if (NextTransitionInShortestPath(state1, state2, NUM_STATES).state == 402 STATE_INVALID) { 403 LOG(ERROR) << "Should be skipping state transitions for: " 404 << GetStateString(state1) << " " << GetStateString(state2); 405 } 406 } 407 } 408 409 output << "\n\nAll transitions:"; 410 output << GetStateTransitionsAsString(); 411 LOG(INFO) << output.str(); 412 } 413 414 // Test that the fullscreen exit bubble is closed by 415 // WindowFullscreenStateChanged() if fullscreen is exited via BrowserWindow. 416 // This currently occurs when an extension exits fullscreen via changing the 417 // browser bounds. 418 TEST_F(FullscreenControllerStateUnitTest, ExitFullscreenViaBrowserWindow) { 419 AddTab(browser(), GURL(content::kAboutBlankURL)); 420 ASSERT_TRUE(InvokeEvent(TOGGLE_FULLSCREEN)); 421 ASSERT_TRUE(InvokeEvent(WINDOW_CHANGE)); 422 ASSERT_TRUE(browser()->window()->IsFullscreen()); 423 // Exit fullscreen without going through fullscreen controller. 424 browser()->window()->ExitFullscreen(); 425 ChangeWindowFullscreenState(); 426 EXPECT_EQ(FEB_TYPE_NONE, 427 browser()->fullscreen_controller()->GetFullscreenExitBubbleType()); 428 } 429 430 // Test that switching tabs takes the browser out of tab fullscreen. 431 TEST_F(FullscreenControllerStateUnitTest, ExitTabFullscreenViaSwitchingTab) { 432 AddTab(browser(), GURL(content::kAboutBlankURL)); 433 AddTab(browser(), GURL(content::kAboutBlankURL)); 434 ASSERT_TRUE(InvokeEvent(TAB_FULLSCREEN_TRUE)); 435 ASSERT_TRUE(InvokeEvent(WINDOW_CHANGE)); 436 ASSERT_TRUE(browser()->window()->IsFullscreen()); 437 438 browser()->tab_strip_model()->SelectNextTab(); 439 ChangeWindowFullscreenState(); 440 EXPECT_FALSE(browser()->window()->IsFullscreen()); 441 } 442 443 // Test that switching tabs via detaching the active tab (which is in tab 444 // fullscreen) takes the browser out of tab fullscreen. This case can 445 // occur if the user is in both tab fullscreen and immersive browser fullscreen. 446 TEST_F(FullscreenControllerStateUnitTest, ExitTabFullscreenViaDetachingTab) { 447 AddTab(browser(), GURL(content::kAboutBlankURL)); 448 AddTab(browser(), GURL(content::kAboutBlankURL)); 449 ASSERT_TRUE(InvokeEvent(TAB_FULLSCREEN_TRUE)); 450 ASSERT_TRUE(InvokeEvent(WINDOW_CHANGE)); 451 ASSERT_TRUE(browser()->window()->IsFullscreen()); 452 453 scoped_ptr<content::WebContents> web_contents( 454 browser()->tab_strip_model()->DetachWebContentsAt(0)); 455 ChangeWindowFullscreenState(); 456 EXPECT_FALSE(browser()->window()->IsFullscreen()); 457 } 458 459 // Test that replacing the web contents for a tab which is in tab fullscreen 460 // takes the browser out of tab fullscreen. This can occur if the user 461 // navigates to a prerendered page from a page which is tab fullscreen. 462 TEST_F(FullscreenControllerStateUnitTest, ExitTabFullscreenViaReplacingTab) { 463 AddTab(browser(), GURL(content::kAboutBlankURL)); 464 ASSERT_TRUE(InvokeEvent(TAB_FULLSCREEN_TRUE)); 465 ASSERT_TRUE(InvokeEvent(WINDOW_CHANGE)); 466 ASSERT_TRUE(browser()->window()->IsFullscreen()); 467 468 content::WebContents* new_web_contents = content::WebContents::Create( 469 content::WebContents::CreateParams(profile())); 470 scoped_ptr<content::WebContents> old_web_contents( 471 browser()->tab_strip_model()->ReplaceWebContentsAt( 472 0, new_web_contents)); 473 ChangeWindowFullscreenState(); 474 EXPECT_FALSE(browser()->window()->IsFullscreen()); 475 } 476