// Copyright (c) 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. #include <memory.h> #include <sstream> #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser_tabstrip.h" #include "chrome/browser/ui/fullscreen/fullscreen_controller.h" #include "chrome/test/base/browser_with_test_window_test.h" #include "content/public/browser/web_contents.h" #include "testing/gtest/include/gtest/gtest.h" // A BrowserWindow used for testing FullscreenController. ---------------------- class FullscreenControllerTestWindow : public TestBrowserWindow { public: // Simulate the window state with an enumeration. enum WindowState { NORMAL, FULLSCREEN, TO_NORMAL, TO_FULLSCREEN }; FullscreenControllerTestWindow(); virtual ~FullscreenControllerTestWindow() {} // BrowserWindow Interface: virtual void EnterFullscreen(const GURL& url, FullscreenExitBubbleType type) OVERRIDE; virtual void ExitFullscreen() OVERRIDE; virtual bool IsFullscreen() const OVERRIDE; #if defined(OS_WIN) virtual void SetMetroSnapMode(bool enable) OVERRIDE; virtual bool IsInMetroSnapMode() const OVERRIDE; #endif static const char* GetWindowStateString(WindowState state); WindowState state() const { return state_; } void set_browser(Browser* browser) { browser_ = browser; } // Simulates the window changing state. void ChangeWindowFullscreenState(); private: WindowState state_; Browser* browser_; }; FullscreenControllerTestWindow::FullscreenControllerTestWindow() : state_(NORMAL), browser_(NULL) { } void FullscreenControllerTestWindow::EnterFullscreen( const GURL& url, FullscreenExitBubbleType type) { if (!IsFullscreen()) state_ = TO_FULLSCREEN; } void FullscreenControllerTestWindow::ExitFullscreen() { if (IsFullscreen()) state_ = TO_NORMAL; } bool FullscreenControllerTestWindow::IsFullscreen() const { return state_ == FULLSCREEN || state_ == TO_NORMAL; } #if defined(OS_WIN) void FullscreenControllerTestWindow::SetMetroSnapMode(bool enable) { NOTREACHED(); // TODO(scheib) Determine how to model Metro Snap. } bool FullscreenControllerTestWindow::IsInMetroSnapMode() const { return false; // TODO(scheib) Determine how to model Metro Snap. } #endif // static const char* FullscreenControllerTestWindow::GetWindowStateString( WindowState state) { switch (state) { case NORMAL: return "NORMAL"; case FULLSCREEN: return "FULLSCREEN"; case TO_FULLSCREEN: return "TO_FULLSCREEN"; case TO_NORMAL: return "TO_NORMAL"; default: NOTREACHED() << "No string for state " << state; return "WindowState-Unknown"; } } void FullscreenControllerTestWindow::ChangeWindowFullscreenState() { // Several states result in "no operation" intentionally. The tests // assume that all possible states and event pairs can be tested, even // though window managers will not generate all of these. switch (state_) { case NORMAL: break; case FULLSCREEN: break; case TO_FULLSCREEN: state_ = FULLSCREEN; break; case TO_NORMAL: state_ = NORMAL; break; default: NOTREACHED(); } // Emit a change event from every state to ensure the Fullscreen Controller // handles it in all circumstances. browser_->WindowFullscreenStateChanged(); } // Unit test fixture for testing Fullscreen Controller. ------------------------ class FullscreenControllerUnitTest : public BrowserWithTestWindowTest { public: enum State { // The window is not in fullscreen. STATE_NORMAL, // User-initiated fullscreen. On Mac, this is Lion-mode for 10.7+. On 10.6, // this is synonymous with STATE_BROWSER_FULLSCREEN_WITH_CHROME. STATE_BROWSER_FULLSCREEN_NO_CHROME, // TO_ states are asynchronous states waiting for window state change // before transitioning to their named state. STATE_TO_NORMAL, STATE_TO_BROWSER_FULLSCREEN_NO_CHROME, NUM_STATES, STATE_INVALID }; enum Event { // FullscreenController::ToggleFullscreenMode() TOGGLE_FULLSCREEN, // FullscreenController::ChangeWindowFullscreenState() WINDOW_CHANGE, NUM_EVENTS, EVENT_INVALID }; virtual void SetUp() OVERRIDE; static const char* GetStateString(State state); static const char* GetEventString(Event event); // Causes Fullscreen Controller to transition to an arbitrary state. void TransitionToState(State state); // Makes one state change to approach |destination_state| via shortest path. // Returns true if a state change is made. // Repeated calls are needed to reach the destination. bool TransitionAStepTowardState(State destination_state); // Causes the |event| to occur and return true on success. bool InvokeEvent(Event event); // Checks that window state matches the expected controller state. void VerifyWindowState(); protected: // Generated information about the transitions between states. struct StateTransitionInfo { StateTransitionInfo() : event(EVENT_INVALID), state(STATE_INVALID), distance(NUM_STATES) {} Event event; // The |Event| that will cause the state transition. State state; // The adjacent |State| transitioned to; not the final state. int distance; // Steps to final state. NUM_STATES represents unknown. }; // Returns next transition info for shortest path from source to destination. StateTransitionInfo NextTransitionInShortestPath(State source, State destination, int search_limit); std::string GetAndClearDebugLog(); FullscreenControllerTestWindow* window_; FullscreenController* fullscreen_controller_; State state_; // Human defined |State| that results given each [state][event] pair. State transition_table_[NUM_STATES][NUM_EVENTS]; // Generated information about the transitions between states [from][to]. StateTransitionInfo state_transitions_[NUM_STATES][NUM_STATES]; // Log of operations reported on errors via GetAndClearDebugLog(). std::ostringstream debugging_log_; }; void FullscreenControllerUnitTest::SetUp() { window_ = new FullscreenControllerTestWindow(); set_window(window_); // BrowserWithTestWindowTest takes ownership. BrowserWithTestWindowTest::SetUp(); window_->set_browser(browser()); fullscreen_controller_ = browser()->fullscreen_controller(); state_ = STATE_NORMAL; // Human specified state machine data. // For each state, for each event, define the resulting state. State transition_table_data[NUM_STATES][NUM_EVENTS] = { // STATE_NORMAL: { STATE_TO_BROWSER_FULLSCREEN_NO_CHROME, // Event TOGGLE_FULLSCREEN STATE_NORMAL }, // Event WINDOW_CHANGE // STATE_BROWSER_FULLSCREEN_NO_CHROME: { STATE_TO_NORMAL, // Event TOGGLE_FULLSCREEN STATE_BROWSER_FULLSCREEN_NO_CHROME }, // Event WINDOW_CHANGE // STATE_TO_NORMAL: { STATE_TO_NORMAL, // Event TOGGLE_FULLSCREEN STATE_NORMAL }, // Event WINDOW_CHANGE // STATE_TO_BROWSER_FULLSCREEN_NO_CHROME: { STATE_TO_BROWSER_FULLSCREEN_NO_CHROME, // Event TOGGLE_FULLSCREEN STATE_BROWSER_FULLSCREEN_NO_CHROME }, // Event WINDOW_CHANGE }; ASSERT_EQ(sizeof(transition_table_data), sizeof(transition_table_)); memcpy(transition_table_, transition_table_data, sizeof(transition_table_data)); // Verify that transition_table_ has been completely defined. for (int source = 0; source < NUM_STATES; source++) { for (int event = 0; event < NUM_EVENTS; event++) { ASSERT_NE(STATE_INVALID, transition_table_[source][event]); ASSERT_LE(0, transition_table_[source][event]); ASSERT_GT(NUM_STATES, transition_table_[source][event]); } } // Copy transition_table_ data into state_transitions_ table. for (int source = 0; source < NUM_STATES; source++) { for (int event = 0; event < NUM_EVENTS; event++) { State destination = transition_table_[source][event]; state_transitions_[source][destination].event = static_cast<Event>(event); state_transitions_[source][destination].state = destination; state_transitions_[source][destination].distance = 1; } } } // static const char* FullscreenControllerUnitTest::GetStateString(State state) { switch (state) { case STATE_NORMAL: return "STATE_NORMAL"; case STATE_BROWSER_FULLSCREEN_NO_CHROME: return "STATE_BROWSER_FULLSCREEN_NO_CHROME"; case STATE_TO_NORMAL: return "STATE_TO_NORMAL"; case STATE_TO_BROWSER_FULLSCREEN_NO_CHROME: return "STATE_TO_BROWSER_FULLSCREEN_NO_CHROME"; case STATE_INVALID: return "STATE_INVALID"; default: NOTREACHED() << "No string for state " << state; return "State-Unknown"; } } // static const char* FullscreenControllerUnitTest::GetEventString(Event event) { switch (event) { case TOGGLE_FULLSCREEN: return "TOGGLE_FULLSCREEN"; case WINDOW_CHANGE: return "WINDOW_CHANGE"; case EVENT_INVALID: return "EVENT_INVALID"; default: NOTREACHED() << "No string for event " << event; return "Event-Unknown"; } } void FullscreenControllerUnitTest::TransitionToState(State final_state) { int max_steps = NUM_STATES; while (max_steps-- && TransitionAStepTowardState(final_state)) continue; ASSERT_GE(max_steps, 0) << "TransitionToState was unable to achieve desired " << "target state. TransitionAStepTowardState iterated too many times." << GetAndClearDebugLog(); ASSERT_EQ(final_state, state_) << "TransitionToState was unable to achieve " << "desired target state. TransitionAStepTowardState returned false." << GetAndClearDebugLog(); } bool FullscreenControllerUnitTest::TransitionAStepTowardState( State destination_state) { State source_state = state_; if (source_state == destination_state) return false; StateTransitionInfo next = NextTransitionInShortestPath(source_state, destination_state, NUM_STATES); if (next.state == STATE_INVALID) { NOTREACHED() << "TransitionAStepTowardState unable to transition. " << "NextTransitionInShortestPath(" << GetStateString(source_state) << ", " << GetStateString(destination_state) << ") returned STATE_INVALID." << GetAndClearDebugLog(); return false; } return InvokeEvent(next.event); } bool FullscreenControllerUnitTest::InvokeEvent(Event event) { State source_state = state_; State next_state = transition_table_[source_state][event]; debugging_log_ << " Transitioning state " << GetStateString(source_state) << " -> " << GetStateString(next_state) << " with event " << GetEventString(event) << std::endl; switch (event) { case TOGGLE_FULLSCREEN: fullscreen_controller_->ToggleFullscreenMode(); break; case WINDOW_CHANGE: window_->ChangeWindowFullscreenState(); break; default: NOTREACHED() << "InvokeEvent needs a handler for event " << GetEventString(event) << GetAndClearDebugLog(); return false; } state_ = next_state; VerifyWindowState(); debugging_log_ << " Window state now " << FullscreenControllerTestWindow::GetWindowStateString(window_->state()) << std::endl; return true; } void FullscreenControllerUnitTest::VerifyWindowState() { switch (state_) { case STATE_NORMAL: EXPECT_EQ(FullscreenControllerTestWindow::NORMAL, window_->state()) << GetAndClearDebugLog(); break; case STATE_BROWSER_FULLSCREEN_NO_CHROME: EXPECT_EQ(FullscreenControllerTestWindow::FULLSCREEN, window_->state()) << GetAndClearDebugLog(); break; case STATE_TO_NORMAL: EXPECT_EQ(FullscreenControllerTestWindow::TO_NORMAL, window_->state()) << GetAndClearDebugLog(); break; case STATE_TO_BROWSER_FULLSCREEN_NO_CHROME: EXPECT_EQ(FullscreenControllerTestWindow::TO_FULLSCREEN, window_->state()) << GetAndClearDebugLog(); break; default: NOTREACHED() << GetAndClearDebugLog(); } } FullscreenControllerUnitTest::StateTransitionInfo FullscreenControllerUnitTest::NextTransitionInShortestPath( State source, State destination, int search_limit) { if (search_limit == 0) return StateTransitionInfo(); // Return a default (invalid) state. if (state_transitions_[source][destination].state == STATE_INVALID) { // Don't know the next state yet, do a depth first search. StateTransitionInfo result; // Consider all states reachable via each event from the source state. for (int event = 0; event < NUM_EVENTS; event++) { State next_state_candidate = transition_table_[source][event]; // Recurse. StateTransitionInfo candidate = NextTransitionInShortestPath( next_state_candidate, destination, search_limit - 1); if (candidate.distance + 1 < result.distance) { result.event = static_cast<Event>(event); result.state = next_state_candidate; result.distance = candidate.distance + 1; } } // Cache result so that a search is not required next time. state_transitions_[source][destination] = result; } return state_transitions_[source][destination]; } std::string FullscreenControllerUnitTest::GetAndClearDebugLog() { debugging_log_ << "(end of log)\n"; std::string output_log = "\nDebugging Log:\n" + debugging_log_.str(); debugging_log_.str(""); return output_log; } // Tests ----------------------------------------------------------------------- TEST_F(FullscreenControllerUnitTest, TransitionsForEachState) { for (int source_int = 0; source_int < NUM_STATES; source_int++) { for (int event_int = 0; event_int < NUM_EVENTS; event_int++) { State state = static_cast<State>(source_int); Event event = static_cast<Event>(event_int); debugging_log_ << "\nTest transition from state " << GetStateString(state) << " via event " << GetEventString(event) << std::endl; debugging_log_ << " First, transition to " << GetStateString(state) << std::endl; ASSERT_NO_FATAL_FAILURE(TransitionToState(state)) << GetAndClearDebugLog(); debugging_log_ << " Then, invoke " << GetEventString(event) << "\n"; ASSERT_TRUE(InvokeEvent(event)) << GetAndClearDebugLog(); } } // Progress of test can be examined via LOG(INFO) << GetAndClearDebugLog(); }