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