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