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/tab_contents/spelling_menu_observer.h" 6 7 #include <vector> 8 9 #include "base/command_line.h" 10 #include "base/prefs/pref_service.h" 11 #include "base/strings/utf_string_conversions.h" 12 #include "chrome/app/chrome_command_ids.h" 13 #include "chrome/browser/spellchecker/spelling_service_client.h" 14 #include "chrome/browser/tab_contents/render_view_context_menu.h" 15 #include "chrome/browser/tab_contents/render_view_context_menu_observer.h" 16 #include "chrome/common/chrome_switches.h" 17 #include "chrome/common/pref_names.h" 18 #include "chrome/test/base/in_process_browser_test.h" 19 #include "chrome/test/base/testing_profile.h" 20 21 using content::RenderViewHost; 22 using content::WebContents; 23 24 namespace { 25 26 // A mock context menu used in this test. This class overrides virtual methods 27 // derived from the RenderViewContextMenuProxy class to monitor calls from the 28 // SpellingMenuObserver class. 29 class MockRenderViewContextMenu : public RenderViewContextMenuProxy { 30 public: 31 // A menu item used in this test. This test uses a vector of this struct to 32 // hold menu items added by this test. 33 struct MockMenuItem { 34 MockMenuItem() 35 : command_id(0), 36 enabled(false), 37 checked(false), 38 hidden(true) { 39 } 40 int command_id; 41 bool enabled; 42 bool checked; 43 bool hidden; 44 string16 title; 45 }; 46 47 MockRenderViewContextMenu(); 48 virtual ~MockRenderViewContextMenu(); 49 50 // RenderViewContextMenuProxy implementation. 51 virtual void AddMenuItem(int command_id, const string16& title) OVERRIDE; 52 virtual void AddCheckItem(int command_id, const string16& title) OVERRIDE; 53 virtual void AddSeparator() OVERRIDE; 54 virtual void AddSubMenu(int command_id, 55 const string16& label, 56 ui::MenuModel* model) OVERRIDE; 57 virtual void UpdateMenuItem(int command_id, 58 bool enabled, 59 bool hidden, 60 const string16& title) OVERRIDE; 61 virtual RenderViewHost* GetRenderViewHost() const OVERRIDE; 62 virtual WebContents* GetWebContents() const OVERRIDE; 63 virtual Profile* GetProfile() const OVERRIDE; 64 65 // Attaches a RenderViewContextMenuObserver to be tested. 66 void SetObserver(RenderViewContextMenuObserver* observer); 67 68 // Returns the number of items added by the test. 69 size_t GetMenuSize() const; 70 71 // Returns the i-th item. 72 bool GetMenuItem(size_t i, MockMenuItem* item) const; 73 74 // Returns the writable profile used in this test. 75 PrefService* GetPrefs(); 76 77 private: 78 // An observer used for initializing the status of menu items added in this 79 // test. A test should delete this RenderViewContextMenuObserver object. 80 RenderViewContextMenuObserver* observer_; 81 82 // A dummy profile used in this test. Call GetPrefs() when a test needs to 83 // change this profile and use PrefService methods. 84 scoped_ptr<TestingProfile> profile_; 85 86 // A list of menu items added by the SpellingMenuObserver class. 87 std::vector<MockMenuItem> items_; 88 89 DISALLOW_COPY_AND_ASSIGN(MockRenderViewContextMenu); 90 }; 91 92 MockRenderViewContextMenu::MockRenderViewContextMenu() 93 : observer_(NULL), 94 profile_(new TestingProfile) { 95 } 96 97 MockRenderViewContextMenu::~MockRenderViewContextMenu() { 98 } 99 100 void MockRenderViewContextMenu::AddMenuItem(int command_id, 101 const string16& title) { 102 MockMenuItem item; 103 item.command_id = command_id; 104 item.enabled = observer_->IsCommandIdEnabled(command_id); 105 item.checked = false; 106 item.hidden = false; 107 item.title = title; 108 items_.push_back(item); 109 } 110 111 void MockRenderViewContextMenu::AddCheckItem(int command_id, 112 const string16& title) { 113 MockMenuItem item; 114 item.command_id = command_id; 115 item.enabled = observer_->IsCommandIdEnabled(command_id); 116 item.checked = observer_->IsCommandIdChecked(command_id); 117 item.hidden = false; 118 item.title = title; 119 items_.push_back(item); 120 } 121 122 void MockRenderViewContextMenu::AddSeparator() { 123 MockMenuItem item; 124 item.command_id = -1; 125 item.enabled = false; 126 item.checked = false; 127 item.hidden = false; 128 items_.push_back(item); 129 } 130 131 void MockRenderViewContextMenu::AddSubMenu(int command_id, 132 const string16& label, 133 ui::MenuModel* model) { 134 MockMenuItem item; 135 item.command_id = -1; 136 item.enabled = false; 137 item.checked = false; 138 item.hidden = false; 139 items_.push_back(item); 140 } 141 142 void MockRenderViewContextMenu::UpdateMenuItem(int command_id, 143 bool enabled, 144 bool hidden, 145 const string16& title) { 146 for (std::vector<MockMenuItem>::iterator it = items_.begin(); 147 it != items_.end(); ++it) { 148 if (it->command_id == command_id) { 149 it->enabled = enabled; 150 it->hidden = hidden; 151 it->title = title; 152 return; 153 } 154 } 155 156 // The SpellingMenuObserver class tries to change a menu item not added by the 157 // class. This is an unexpected behavior and we should stop now. 158 FAIL(); 159 } 160 161 RenderViewHost* MockRenderViewContextMenu::GetRenderViewHost() const { 162 return NULL; 163 } 164 165 WebContents* MockRenderViewContextMenu::GetWebContents() const { 166 return NULL; 167 } 168 169 Profile* MockRenderViewContextMenu::GetProfile() const { 170 return profile_.get(); 171 } 172 173 size_t MockRenderViewContextMenu::GetMenuSize() const { 174 return items_.size(); 175 } 176 177 bool MockRenderViewContextMenu::GetMenuItem(size_t i, 178 MockMenuItem* item) const { 179 if (i >= items_.size()) 180 return false; 181 item->command_id = items_[i].command_id; 182 item->enabled = items_[i].enabled; 183 item->checked = items_[i].checked; 184 item->hidden = items_[i].hidden; 185 item->title = items_[i].title; 186 return true; 187 } 188 189 void MockRenderViewContextMenu::SetObserver( 190 RenderViewContextMenuObserver* observer) { 191 observer_ = observer; 192 } 193 194 PrefService* MockRenderViewContextMenu::GetPrefs() { 195 return profile_->GetPrefs(); 196 } 197 198 // A test class used in this file. This test should be a browser test because it 199 // accesses resources. 200 class SpellingMenuObserverTest : public InProcessBrowserTest { 201 public: 202 SpellingMenuObserverTest(); 203 204 virtual void SetUpOnMainThread() OVERRIDE { 205 Reset(); 206 } 207 208 virtual void CleanUpOnMainThread() OVERRIDE { 209 observer_.reset(); 210 menu_.reset(); 211 } 212 213 void Reset() { 214 observer_.reset(); 215 menu_.reset(new MockRenderViewContextMenu); 216 observer_.reset(new SpellingMenuObserver(menu_.get())); 217 menu_->SetObserver(observer_.get()); 218 } 219 220 void InitMenu(const char* word, const char* suggestion) { 221 content::ContextMenuParams params; 222 params.is_editable = true; 223 params.misspelled_word = ASCIIToUTF16(word); 224 params.dictionary_suggestions.clear(); 225 if (suggestion) 226 params.dictionary_suggestions.push_back(ASCIIToUTF16(suggestion)); 227 observer_->InitMenu(params); 228 } 229 230 virtual ~SpellingMenuObserverTest(); 231 MockRenderViewContextMenu* menu() { return menu_.get(); } 232 SpellingMenuObserver* observer() { return observer_.get(); } 233 private: 234 scoped_ptr<SpellingMenuObserver> observer_; 235 scoped_ptr<MockRenderViewContextMenu> menu_; 236 DISALLOW_COPY_AND_ASSIGN(SpellingMenuObserverTest); 237 }; 238 239 SpellingMenuObserverTest::SpellingMenuObserverTest() { 240 } 241 242 SpellingMenuObserverTest::~SpellingMenuObserverTest() { 243 } 244 245 } // namespace 246 247 // Tests that right-clicking a correct word does not add any items. 248 IN_PROC_BROWSER_TEST_F(SpellingMenuObserverTest, InitMenuWithCorrectWord) { 249 InitMenu("", NULL); 250 EXPECT_EQ(static_cast<size_t>(0), menu()->GetMenuSize()); 251 } 252 253 // Tests that right-clicking a misspelled word adds four items: 254 // "No spelling suggestions", "Add to dictionary", "Ask Google for suggestions", 255 // and a separator. 256 IN_PROC_BROWSER_TEST_F(SpellingMenuObserverTest, InitMenuWithMisspelledWord) { 257 InitMenu("wiimode", NULL); 258 EXPECT_EQ(static_cast<size_t>(4), menu()->GetMenuSize()); 259 260 // Read all the context-menu items added by this test and verify they are 261 // expected ones. We do not check the item titles to prevent resource changes 262 // from breaking this test. (I think it is not expected by those who change 263 // resources.) 264 MockRenderViewContextMenu::MockMenuItem item; 265 menu()->GetMenuItem(0, &item); 266 EXPECT_EQ(IDC_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS, item.command_id); 267 EXPECT_FALSE(item.enabled); 268 EXPECT_FALSE(item.hidden); 269 menu()->GetMenuItem(1, &item); 270 EXPECT_EQ(IDC_SPELLCHECK_ADD_TO_DICTIONARY, item.command_id); 271 EXPECT_TRUE(item.enabled); 272 EXPECT_FALSE(item.hidden); 273 menu()->GetMenuItem(2, &item); 274 EXPECT_EQ(IDC_CONTENT_CONTEXT_SPELLING_TOGGLE, item.command_id); 275 EXPECT_TRUE(item.enabled); 276 EXPECT_FALSE(item.checked); 277 EXPECT_FALSE(item.hidden); 278 menu()->GetMenuItem(3, &item); 279 EXPECT_EQ(-1, item.command_id); 280 EXPECT_FALSE(item.enabled); 281 EXPECT_FALSE(item.hidden); 282 } 283 284 // Tests that right-clicking a correct word when we enable spelling-service 285 // integration to verify an item "Ask Google for suggestions" is checked. Even 286 // though this meanu itself does not add this item, its sub-menu adds the item 287 // and calls SpellingMenuObserver::IsChecked() to check it. 288 IN_PROC_BROWSER_TEST_F(SpellingMenuObserverTest, 289 EnableSpellingServiceWithCorrectWord) { 290 menu()->GetPrefs()->SetBoolean(prefs::kSpellCheckUseSpellingService, true); 291 InitMenu("", NULL); 292 293 EXPECT_TRUE( 294 observer()->IsCommandIdChecked(IDC_CONTENT_CONTEXT_SPELLING_TOGGLE)); 295 } 296 297 // Tests that right-clicking a misspelled word when we enable spelling-service 298 // integration to verify an item "Ask Google for suggestions" is checked. (This 299 // test does not actually send JSON-RPC requests to the service because it makes 300 // this test flaky.) 301 IN_PROC_BROWSER_TEST_F(SpellingMenuObserverTest, EnableSpellingService) { 302 menu()->GetPrefs()->SetBoolean(prefs::kSpellCheckUseSpellingService, true); 303 menu()->GetPrefs()->SetString(prefs::kSpellCheckDictionary, std::string()); 304 305 InitMenu("wiimode", NULL); 306 EXPECT_EQ(static_cast<size_t>(4), menu()->GetMenuSize()); 307 308 // To avoid duplicates, this test reads only the "Ask Google for suggestions" 309 // item and verifies it is enabled and checked. 310 MockRenderViewContextMenu::MockMenuItem item; 311 menu()->GetMenuItem(2, &item); 312 EXPECT_EQ(IDC_CONTENT_CONTEXT_SPELLING_TOGGLE, item.command_id); 313 EXPECT_TRUE(item.enabled); 314 EXPECT_TRUE(item.checked); 315 EXPECT_FALSE(item.hidden); 316 } 317 318 // Test that there will be a separator after "no suggestions" if 319 // SpellingServiceClient::SUGGEST is on. 320 IN_PROC_BROWSER_TEST_F(SpellingMenuObserverTest, SeparatorAfterSuggestions) { 321 menu()->GetPrefs()->SetBoolean(prefs::kSpellCheckUseSpellingService, true); 322 CommandLine* command_line = CommandLine::ForCurrentProcess(); 323 command_line->AppendSwitch(switches::kUseSpellingSuggestions); 324 325 // Force a non-empty locale so SUGGEST is available. 326 menu()->GetPrefs()->SetString(prefs::kSpellCheckDictionary, "en"); 327 EXPECT_TRUE(SpellingServiceClient::IsAvailable(menu()->GetProfile(), 328 SpellingServiceClient::SUGGEST)); 329 330 InitMenu("jhhj", NULL); 331 332 // The test should see a top separator, "No spelling suggestions", 333 // "No more Google suggestions" (from SpellingService) and a separator 334 // as the first four items, then possibly more (not relevant here). 335 EXPECT_LT(4U, menu()->GetMenuSize()); 336 337 MockRenderViewContextMenu::MockMenuItem item; 338 menu()->GetMenuItem(0, &item); 339 EXPECT_EQ(-1, item.command_id); 340 EXPECT_FALSE(item.enabled); 341 EXPECT_FALSE(item.hidden); 342 343 menu()->GetMenuItem(1, &item); 344 EXPECT_EQ(IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION, item.command_id); 345 EXPECT_FALSE(item.enabled); 346 EXPECT_FALSE(item.hidden); 347 348 menu()->GetMenuItem(2, &item); 349 EXPECT_EQ(IDC_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS, item.command_id); 350 EXPECT_FALSE(item.enabled); 351 EXPECT_FALSE(item.hidden); 352 353 menu()->GetMenuItem(3, &item); 354 EXPECT_EQ(-1, item.command_id); 355 EXPECT_FALSE(item.enabled); 356 EXPECT_FALSE(item.hidden); 357 } 358 359 // Test that we don't show "No more suggestions from Google" if the spelling 360 // service is enabled and that there is only one suggestion. 361 IN_PROC_BROWSER_TEST_F(SpellingMenuObserverTest, 362 NoMoreSuggestionsNotDisplayed) { 363 menu()->GetPrefs()->SetBoolean(prefs::kSpellCheckUseSpellingService, true); 364 365 // Force a non-empty locale so SPELLCHECK is available. 366 menu()->GetPrefs()->SetString(prefs::kSpellCheckDictionary, "en"); 367 EXPECT_TRUE(SpellingServiceClient::IsAvailable(menu()->GetProfile(), 368 SpellingServiceClient::SPELLCHECK)); 369 InitMenu("asdfkj", "asdf"); 370 371 // The test should see a separator, a suggestion and another separator 372 // as the first two items, then possibly more (not relevant here). 373 EXPECT_LT(3U, menu()->GetMenuSize()); 374 375 MockRenderViewContextMenu::MockMenuItem item; 376 menu()->GetMenuItem(0, &item); 377 EXPECT_EQ(-1, item.command_id); 378 EXPECT_FALSE(item.enabled); 379 EXPECT_FALSE(item.hidden); 380 381 menu()->GetMenuItem(1, &item); 382 EXPECT_EQ(IDC_SPELLCHECK_SUGGESTION_0, item.command_id); 383 EXPECT_TRUE(item.enabled); 384 EXPECT_FALSE(item.hidden); 385 386 menu()->GetMenuItem(2, &item); 387 EXPECT_EQ(-1, item.command_id); 388 EXPECT_FALSE(item.enabled); 389 EXPECT_FALSE(item.hidden); 390 } 391 392 // Test that "Ask Google For Suggestions" is grayed out when using an 393 // off the record profile. 394 // TODO(rlp): Include graying out of autocorrect in this test when autocorrect 395 // is functional. 396 IN_PROC_BROWSER_TEST_F(SpellingMenuObserverTest, 397 NoSpellingServiceWhenOffTheRecord) { 398 menu()->GetProfile()->AsTestingProfile()->set_incognito(true); 399 400 // This means spellchecking is allowed. Default is that the service is 401 // contacted but this test makes sure that if profile is incognito, that 402 // is not an option. 403 menu()->GetPrefs()->SetBoolean(prefs::kSpellCheckUseSpellingService, true); 404 405 // Force a non-empty locale so SUGGEST normally would be available. 406 menu()->GetPrefs()->SetString(prefs::kSpellCheckDictionary, "en"); 407 EXPECT_FALSE(SpellingServiceClient::IsAvailable(menu()->GetProfile(), 408 SpellingServiceClient::SUGGEST)); 409 EXPECT_FALSE(SpellingServiceClient::IsAvailable(menu()->GetProfile(), 410 SpellingServiceClient::SPELLCHECK)); 411 412 InitMenu("sjxdjiiiiii", NULL); 413 414 // The test should see "No spelling suggestions" (from system checker). 415 // They should not see "No more Google suggestions" (from SpellingService) or 416 // a separator. The next 2 items should be "Add to Dictionary" followed 417 // by "Ask Google for suggestions" which should be disabled. 418 // TODO(rlp): add autocorrect here when it is functional. 419 EXPECT_LT(3U, menu()->GetMenuSize()); 420 421 MockRenderViewContextMenu::MockMenuItem item; 422 menu()->GetMenuItem(0, &item); 423 EXPECT_EQ(IDC_CONTENT_CONTEXT_NO_SPELLING_SUGGESTIONS, item.command_id); 424 EXPECT_FALSE(item.enabled); 425 EXPECT_FALSE(item.hidden); 426 427 menu()->GetMenuItem(1, &item); 428 EXPECT_EQ(IDC_SPELLCHECK_ADD_TO_DICTIONARY, item.command_id); 429 EXPECT_TRUE(item.enabled); 430 EXPECT_FALSE(item.hidden); 431 432 menu()->GetMenuItem(2, &item); 433 EXPECT_EQ(IDC_CONTENT_CONTEXT_SPELLING_TOGGLE, item.command_id); 434 EXPECT_FALSE(item.enabled); 435 EXPECT_FALSE(item.hidden); 436 437 // Set incognito back to false to allow appropriate test cleanup. 438 menu()->GetProfile()->AsTestingProfile()->set_incognito(false); 439 } 440 441 // Test that the menu is preceeded by a separator if there are any suggestions, 442 // or if the SpellingServiceClient is available 443 IN_PROC_BROWSER_TEST_F(SpellingMenuObserverTest, SuggestionsForceTopSeparator) { 444 menu()->GetPrefs()->SetBoolean(prefs::kSpellCheckUseSpellingService, false); 445 446 // First case: Misspelled word, no suggestions, no spellcheck service. 447 InitMenu("asdfkj", NULL); 448 // See SpellingMenuObserverTest.InitMenuWithMisspelledWord on why 4 items. 449 EXPECT_EQ(static_cast<size_t>(4), menu()->GetMenuSize()); 450 MockRenderViewContextMenu::MockMenuItem item; 451 menu()->GetMenuItem(0, &item); 452 EXPECT_NE(-1, item.command_id); 453 454 // Case #2. Misspelled word, suggestions, no spellcheck service. 455 Reset(); 456 menu()->GetPrefs()->SetBoolean(prefs::kSpellCheckUseSpellingService, false); 457 InitMenu("asdfkj", "asdf"); 458 459 // Expect at least separator and 4 default entries. 460 EXPECT_LT(static_cast<size_t>(5), menu()->GetMenuSize()); 461 // This test only cares that the first one is a separator. 462 menu()->GetMenuItem(0, &item); 463 EXPECT_EQ(-1, item.command_id); 464 465 // Case #3. Misspelled word, suggestion service is on. 466 Reset(); 467 menu()->GetPrefs()->SetBoolean(prefs::kSpellCheckUseSpellingService, true); 468 CommandLine* command_line = CommandLine::ForCurrentProcess(); 469 command_line->AppendSwitch(switches::kUseSpellingSuggestions); 470 InitMenu("asdfkj", NULL); 471 472 // Should have at least 2 entries. Separator, suggestion. 473 EXPECT_LT(2U, menu()->GetMenuSize()); 474 menu()->GetMenuItem(0, &item); 475 EXPECT_EQ(-1, item.command_id); 476 menu()->GetMenuItem(1, &item); 477 EXPECT_EQ(IDC_CONTENT_CONTEXT_SPELLING_SUGGESTION, item.command_id); 478 } 479