spelling_menu_observer_browsertest.cc revision 1320f92c476a1ad9d19dba2a48c72b75566198e9
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/prefs/pref_service.h"
10#include "base/strings/utf_string_conversions.h"
11#include "chrome/app/chrome_command_ids.h"
12#include "chrome/browser/renderer_context_menu/render_view_context_menu.h"
13#include "chrome/browser/spellchecker/spelling_service_client.h"
14#include "chrome/common/pref_names.h"
15#include "chrome/test/base/in_process_browser_test.h"
16#include "chrome/test/base/testing_profile.h"
17#include "components/renderer_context_menu/render_view_context_menu_observer.h"
18
19using content::RenderViewHost;
20using content::WebContents;
21
22namespace {
23
24// A mock context menu used in this test. This class overrides virtual methods
25// derived from the RenderViewContextMenuProxy class to monitor calls from the
26// SpellingMenuObserver class.
27class MockRenderViewContextMenu : public RenderViewContextMenuProxy {
28 public:
29  // A menu item used in this test. This test uses a vector of this struct to
30  // hold menu items added by this test.
31  struct MockMenuItem {
32    MockMenuItem()
33        : command_id(0),
34          enabled(false),
35          checked(false),
36          hidden(true) {
37    }
38    int command_id;
39    bool enabled;
40    bool checked;
41    bool hidden;
42    base::string16 title;
43  };
44
45  explicit MockRenderViewContextMenu(bool incognito);
46  virtual ~MockRenderViewContextMenu();
47
48  // RenderViewContextMenuProxy implementation.
49  virtual void AddMenuItem(int command_id,
50                           const base::string16& title) OVERRIDE;
51  virtual void AddCheckItem(int command_id,
52                            const base::string16& title) OVERRIDE;
53  virtual void AddSeparator() OVERRIDE;
54  virtual void AddSubMenu(int command_id,
55                          const base::string16& label,
56                          ui::MenuModel* model) OVERRIDE;
57  virtual void UpdateMenuItem(int command_id,
58                              bool enabled,
59                              bool hidden,
60                              const base::string16& title) OVERRIDE;
61  virtual RenderViewHost* GetRenderViewHost() const OVERRIDE;
62  virtual WebContents* GetWebContents() const OVERRIDE;
63  virtual content::BrowserContext* GetBrowserContext() 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> original_profile_;
85
86  // Either |original_profile_| or its incognito profile.
87  Profile* profile_;
88
89  // A list of menu items added by the SpellingMenuObserver class.
90  std::vector<MockMenuItem> items_;
91
92  DISALLOW_COPY_AND_ASSIGN(MockRenderViewContextMenu);
93};
94
95MockRenderViewContextMenu::MockRenderViewContextMenu(bool incognito)
96    : observer_(NULL) {
97  original_profile_ = TestingProfile::Builder().Build();
98  profile_ = incognito ? original_profile_->GetOffTheRecordProfile()
99                       : original_profile_.get();
100}
101
102MockRenderViewContextMenu::~MockRenderViewContextMenu() {
103}
104
105void 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
116void 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
127void 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
136void 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
147void 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
166RenderViewHost* MockRenderViewContextMenu::GetRenderViewHost() const {
167  return NULL;
168}
169
170WebContents* MockRenderViewContextMenu::GetWebContents() const {
171  return NULL;
172}
173
174content::BrowserContext* MockRenderViewContextMenu::GetBrowserContext() const {
175  return profile_;
176}
177
178size_t MockRenderViewContextMenu::GetMenuSize() const {
179  return items_.size();
180}
181
182bool 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
194void MockRenderViewContextMenu::SetObserver(
195    RenderViewContextMenuObserver* observer) {
196  observer_ = observer;
197}
198
199PrefService* 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.
205class SpellingMenuObserverTest : public InProcessBrowserTest {
206 public:
207  SpellingMenuObserverTest();
208
209  virtual void SetUpOnMainThread() OVERRIDE {
210    Reset(false);
211  }
212
213  virtual void TearDownOnMainThread() 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()->GetBrowserContext(), SpellingServiceClient::SUGGEST));
241    ASSERT_FALSE(SpellingServiceClient::IsAvailable(
242        menu()->GetBrowserContext(), 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
254SpellingMenuObserverTest::SpellingMenuObserverTest() {
255}
256
257SpellingMenuObserverTest::~SpellingMenuObserverTest() {
258}
259
260}  // namespace
261
262// Tests that right-clicking a correct word does not add any items.
263IN_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.
271IN_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.
303IN_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.)
316IN_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.
335IN_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.
368IN_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(
375      menu()->GetBrowserContext(), 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.
403IN_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(
416      menu()->GetBrowserContext(), SpellingServiceClient::SUGGEST));
417  EXPECT_FALSE(SpellingServiceClient::IsAvailable(
418      menu()->GetBrowserContext(), 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
448IN_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