apps_search_results_controller_unittest.mm revision 424c4d7b64af9d0d8fd9624f381f469654d5e3d2
1// Copyright 2013 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#import "ui/app_list/cocoa/apps_search_results_controller.h"
6
7#include "base/mac/scoped_nsobject.h"
8#include "base/message_loop/message_loop.h"
9#include "base/strings/stringprintf.h"
10#include "base/strings/sys_string_conversions.h"
11#include "base/strings/utf_string_conversions.h"
12#import "testing/gtest_mac.h"
13#include "ui/app_list/search_result.h"
14#include "ui/app_list/test/app_list_test_model.h"
15#include "ui/base/models/simple_menu_model.h"
16#import "ui/base/test/ui_cocoa_test_helper.h"
17#include "ui/gfx/image/image_skia_util_mac.h"
18#import "ui/base/test/cocoa_test_event_utils.h"
19
20@interface TestAppsSearchResultsDelegate : NSObject<AppsSearchResultsDelegate> {
21 @private
22  app_list::test::AppListTestModel appListModel_;
23  app_list::SearchResult* lastOpenedResult_;
24  int redoSearchCount_;
25}
26
27@property(readonly, nonatomic) app_list::SearchResult* lastOpenedResult;
28@property(readonly, nonatomic) int redoSearchCount;
29
30@end
31
32@implementation TestAppsSearchResultsDelegate
33
34@synthesize lastOpenedResult = lastOpenedResult_;
35@synthesize redoSearchCount = redoSearchCount_;
36
37- (app_list::AppListModel*)appListModel {
38  return &appListModel_;
39}
40
41- (void)openResult:(app_list::SearchResult*)result {
42  lastOpenedResult_ = result;
43}
44
45- (void)redoSearch {
46  ++redoSearchCount_;
47}
48
49@end
50
51namespace app_list {
52namespace test {
53namespace {
54
55const int kDefaultResultsCount = 3;
56
57class SearchResultWithMenu : public SearchResult {
58 public:
59  SearchResultWithMenu(const std::string& title, const std::string& details)
60      : menu_model_(NULL),
61        menu_ready_(true) {
62    set_title(ASCIIToUTF16(title));
63    set_details(ASCIIToUTF16(details));
64    menu_model_.AddItem(0, UTF8ToUTF16("Menu For: " + title));
65  }
66
67  void SetMenuReadyForTesting(bool ready) {
68    menu_ready_ = ready;
69  }
70
71  virtual ui::MenuModel* GetContextMenuModel() OVERRIDE {
72    if (!menu_ready_)
73      return NULL;
74
75    return &menu_model_;
76  }
77
78 private:
79  ui::SimpleMenuModel menu_model_;
80  bool menu_ready_;
81
82  DISALLOW_COPY_AND_ASSIGN(SearchResultWithMenu);
83};
84
85class AppsSearchResultsControllerTest : public ui::CocoaTest {
86 public:
87  AppsSearchResultsControllerTest() {}
88
89  void AddTestResultAtIndex(size_t index,
90                            const std::string& title,
91                            const std::string& details) {
92    scoped_ptr<SearchResult> result(new SearchResultWithMenu(title, details));
93    AppListModel::SearchResults* results = [delegate_ appListModel]->results();
94    results->AddAt(index, result.release());
95  }
96
97  SearchResult* ModelResultAt(size_t index) {
98    return [delegate_ appListModel]->results()->GetItemAt(index);
99  }
100
101  NSCell* ViewResultAt(NSInteger index) {
102    NSTableView* table_view = [apps_search_results_controller_ tableView];
103    return [table_view preparedCellAtColumn:0
104                                        row:index];
105  }
106
107  void SetMenuReadyAt(size_t index, bool ready) {
108    SearchResultWithMenu* result =
109        static_cast<SearchResultWithMenu*>(ModelResultAt(index));
110    result->SetMenuReadyForTesting(ready);
111  }
112
113  BOOL SimulateKeyAction(SEL c) {
114    return [apps_search_results_controller_ handleCommandBySelector:c];
115  }
116
117  void ExpectConsistent();
118
119  // ui::CocoaTest overrides:
120  virtual void SetUp() OVERRIDE;
121  virtual void TearDown() OVERRIDE;
122
123 protected:
124  base::scoped_nsobject<TestAppsSearchResultsDelegate> delegate_;
125  base::scoped_nsobject<AppsSearchResultsController>
126      apps_search_results_controller_;
127
128 private:
129  DISALLOW_COPY_AND_ASSIGN(AppsSearchResultsControllerTest);
130};
131
132void AppsSearchResultsControllerTest::ExpectConsistent() {
133  NSInteger item_count = [delegate_ appListModel]->results()->item_count();
134  ASSERT_EQ(item_count,
135            [[apps_search_results_controller_ tableView] numberOfRows]);
136
137  // Compare content strings to ensure the order of items is consistent, and any
138  // model data that should have been reloaded has been reloaded in the view.
139  for (NSInteger i = 0; i < item_count; ++i) {
140    SearchResult* result = ModelResultAt(i);
141    base::string16 string_in_model = result->title();
142    if (!result->details().empty())
143      string_in_model += ASCIIToUTF16("\n") + result->details();
144    EXPECT_NSEQ(base::SysUTF16ToNSString(string_in_model),
145                [[ViewResultAt(i) attributedStringValue] string]);
146  }
147}
148
149void AppsSearchResultsControllerTest::SetUp() {
150  apps_search_results_controller_.reset(
151      [[AppsSearchResultsController alloc] initWithAppsSearchResultsFrameSize:
152          NSMakeSize(400, 400)]);
153  // The view is initially hidden. Give it a non-zero height so it draws.
154  [[apps_search_results_controller_ view] setFrameSize:NSMakeSize(400, 400)];
155
156  delegate_.reset([[TestAppsSearchResultsDelegate alloc] init]);
157
158  // Populate with some results so that TEST_VIEW does something non-trivial.
159  for (int i = 0; i < kDefaultResultsCount; ++i)
160    AddTestResultAtIndex(i, base::StringPrintf("Result %d", i), "ItemDetail");
161
162  SearchResult::Tags test_tags;
163  // Apply markup to the substring "Result" in the first item.
164  test_tags.push_back(SearchResult::Tag(SearchResult::Tag::NONE, 0, 1));
165  test_tags.push_back(SearchResult::Tag(SearchResult::Tag::URL, 1, 2));
166  test_tags.push_back(SearchResult::Tag(SearchResult::Tag::MATCH, 2, 3));
167  test_tags.push_back(SearchResult::Tag(SearchResult::Tag::DIM, 3, 4));
168  test_tags.push_back(SearchResult::Tag(SearchResult::Tag::MATCH |
169                                        SearchResult::Tag::URL, 4, 5));
170  test_tags.push_back(SearchResult::Tag(SearchResult::Tag::MATCH |
171                                        SearchResult::Tag::DIM, 5, 6));
172
173  SearchResult* result = ModelResultAt(0);
174  result->SetIcon(gfx::ImageSkiaFromNSImage(
175      [NSImage imageNamed:NSImageNameStatusAvailable]));
176  result->set_title_tags(test_tags);
177
178  [apps_search_results_controller_ setDelegate:delegate_];
179
180  ui::CocoaTest::SetUp();
181  [[test_window() contentView] addSubview:
182      [apps_search_results_controller_ view]];
183}
184
185void AppsSearchResultsControllerTest::TearDown() {
186  [apps_search_results_controller_ setDelegate:nil];
187  ui::CocoaTest::TearDown();
188}
189
190NSEvent* MouseEventInRow(NSTableView* table_view, NSInteger row_index) {
191  NSRect row_rect = [table_view rectOfRow:row_index];
192  NSPoint point_in_view = NSMakePoint(NSMidX(row_rect), NSMidY(row_rect));
193  NSPoint point_in_window = [table_view convertPoint:point_in_view
194                                              toView:nil];
195  return cocoa_test_event_utils::LeftMouseDownAtPoint(point_in_window);
196}
197
198}  // namespace
199
200TEST_VIEW(AppsSearchResultsControllerTest,
201          [apps_search_results_controller_ view]);
202
203TEST_F(AppsSearchResultsControllerTest, ModelObservers) {
204  NSTableView* table_view = [apps_search_results_controller_ tableView];
205  ExpectConsistent();
206
207  EXPECT_EQ(1, [table_view numberOfColumns]);
208  EXPECT_EQ(kDefaultResultsCount, [table_view numberOfRows]);
209
210  // Insert at start.
211  AddTestResultAtIndex(0, "One", std::string());
212  EXPECT_EQ(kDefaultResultsCount + 1, [table_view numberOfRows]);
213  ExpectConsistent();
214
215  // Remove from end.
216  [delegate_ appListModel]->results()->DeleteAt(kDefaultResultsCount);
217  EXPECT_EQ(kDefaultResultsCount, [table_view numberOfRows]);
218  ExpectConsistent();
219
220  // Insert at end.
221  AddTestResultAtIndex(kDefaultResultsCount, "Four", std::string());
222  EXPECT_EQ(kDefaultResultsCount + 1, [table_view numberOfRows]);
223  ExpectConsistent();
224
225  // Delete from start.
226  [delegate_ appListModel]->results()->DeleteAt(0);
227  EXPECT_EQ(kDefaultResultsCount, [table_view numberOfRows]);
228  ExpectConsistent();
229
230  // Test clearing results.
231  [delegate_ appListModel]->results()->DeleteAll();
232  EXPECT_EQ(0, [table_view numberOfRows]);
233  ExpectConsistent();
234}
235
236TEST_F(AppsSearchResultsControllerTest, KeyboardSelectAndActivate) {
237  NSTableView* table_view = [apps_search_results_controller_ tableView];
238  EXPECT_EQ(-1, [table_view selectedRow]);
239
240  // Pressing up when nothing is selected should select the last item.
241  EXPECT_TRUE(SimulateKeyAction(@selector(moveUp:)));
242  EXPECT_EQ(kDefaultResultsCount - 1, [table_view selectedRow]);
243  [table_view deselectAll:nil];
244  EXPECT_EQ(-1, [table_view selectedRow]);
245
246  // Pressing down when nothing is selected should select the first item.
247  EXPECT_TRUE(SimulateKeyAction(@selector(moveDown:)));
248  EXPECT_EQ(0, [table_view selectedRow]);
249
250  // Pressing up should wrap around.
251  EXPECT_TRUE(SimulateKeyAction(@selector(moveUp:)));
252  EXPECT_EQ(kDefaultResultsCount - 1, [table_view selectedRow]);
253
254  // Down should now also wrap, since the selection is at the end.
255  EXPECT_TRUE(SimulateKeyAction(@selector(moveDown:)));
256  EXPECT_EQ(0, [table_view selectedRow]);
257
258  // Regular down and up movement, ensuring the cells have correct backgrounds.
259  EXPECT_TRUE(SimulateKeyAction(@selector(moveDown:)));
260  EXPECT_EQ(1, [table_view selectedRow]);
261  EXPECT_EQ(NSBackgroundStyleDark, [ViewResultAt(1) backgroundStyle]);
262  EXPECT_EQ(NSBackgroundStyleLight, [ViewResultAt(0) backgroundStyle]);
263
264  EXPECT_TRUE(SimulateKeyAction(@selector(moveUp:)));
265  EXPECT_EQ(0, [table_view selectedRow]);
266  EXPECT_EQ(NSBackgroundStyleDark, [ViewResultAt(0) backgroundStyle]);
267  EXPECT_EQ(NSBackgroundStyleLight, [ViewResultAt(1) backgroundStyle]);
268
269  // Test activating items.
270  EXPECT_TRUE(SimulateKeyAction(@selector(insertNewline:)));
271  EXPECT_EQ(ModelResultAt(0), [delegate_ lastOpenedResult]);
272  EXPECT_TRUE(SimulateKeyAction(@selector(moveDown:)));
273  EXPECT_TRUE(SimulateKeyAction(@selector(insertNewline:)));
274  EXPECT_EQ(ModelResultAt(1), [delegate_ lastOpenedResult]);
275}
276
277TEST_F(AppsSearchResultsControllerTest, ContextMenus) {
278  NSTableView* table_view = [apps_search_results_controller_ tableView];
279  NSEvent* mouse_in_row_0 = MouseEventInRow(table_view, 0);
280  NSEvent* mouse_in_row_1 = MouseEventInRow(table_view, 1);
281
282  NSMenu* menu = [table_view menuForEvent:mouse_in_row_0];
283  EXPECT_EQ(1, [menu numberOfItems]);
284  EXPECT_NSEQ(@"Menu For: Result 0", [[menu itemAtIndex:0] title]);
285
286  // Test a context menu request while the item is still installing.
287  SetMenuReadyAt(1, false);
288  menu = [table_view menuForEvent:mouse_in_row_1];
289  EXPECT_EQ(nil, menu);
290
291  SetMenuReadyAt(1, true);
292  menu = [table_view menuForEvent:mouse_in_row_1];
293  EXPECT_EQ(1, [menu numberOfItems]);
294  EXPECT_NSEQ(@"Menu For: Result 1", [[menu itemAtIndex:0] title]);
295}
296
297// Test that observing a search result item uninstall performs the search again.
298TEST_F(AppsSearchResultsControllerTest, UninstallRedperformsSearch) {
299  base::MessageLoopForUI message_loop;
300  EXPECT_EQ(0, [delegate_ redoSearchCount]);
301  ModelResultAt(0)->NotifyItemUninstalled();
302  message_loop.PostTask(FROM_HERE, base::MessageLoop::QuitClosure());
303  message_loop.Run();
304  EXPECT_EQ(1, [delegate_ redoSearchCount]);
305}
306
307}  // namespace test
308}  // namespace app_list
309